Calendar/wwwroot/poc-website-builder.html
Janus C. H. Knudsen 0137a4b4f9 Adds HR tab and website builder prototype
Introduces new HR section in employee profile with documents, certifications, and courses management

Adds initial website builder interface with block-based design system and theming capabilities

Enhances settings and indstillinger pages with new module configurations
2026-01-03 16:18:53 +01:00

3633 lines
110 KiB
HTML

<!DOCTYPE html>
<html lang="da">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Website Builder - Salon OS</title>
<link href="https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Playfair+Display:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet">
<link href="https://fonts.googleapis.com/css2?family=Montserrat:wght@400;500;600;700&display=swap" rel="stylesheet">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@phosphor-icons/web@2.1.2/src/regular/style.css" />
<style>
/* ==========================================
FONT FACE (Poppins)
========================================== */
@font-face {
font-family: 'Poppins';
src: url('fonts/Poppins-Regular.woff') format('woff');
font-weight: 400;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('fonts/Poppins-Medium.woff') format('woff');
font-weight: 500;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('fonts/Poppins-SemiBold.woff') format('woff');
font-weight: 600;
font-style: normal;
font-display: swap;
}
@font-face {
font-family: 'Poppins';
src: url('fonts/Poppins-Bold.woff') format('woff');
font-weight: 700;
font-style: normal;
font-display: swap;
}
/* ==========================================
CSS VARIABLES
========================================== */
:root {
--color-surface: #fff;
--color-background: #f5f5f5;
--color-background-hover: #f0f0f0;
--color-background-alt: #fafafa;
--color-border: #e0e0e0;
--color-text: #333;
--color-text-secondary: #666;
--color-text-muted: #999;
--color-teal: #00897b;
--color-blue: #1976d2;
--color-red: #e53935;
--color-amber: #f59e0b;
--color-purple: #8b5cf6;
--color-green: #43a047;
--font-family: 'Poppins', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
--font-mono: 'JetBrains Mono', monospace;
/* Builder specific */
--builder-topbar-height: 56px;
--builder-library-width: 280px;
--builder-settings-width: 320px;
/* Theme variables (user customizable) */
--theme-primary: #00897b;
--theme-secondary: #333333;
--theme-accent: #f59e0b;
--theme-font: 'Poppins', sans-serif;
}
/* ==========================================
RESET & BASE
========================================== */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: var(--font-family);
font-size: 14px;
color: var(--color-text);
background: var(--color-background);
line-height: 1.5;
overflow: hidden;
}
/* ==========================================
BUILDER LAYOUT
========================================== */
swp-website-builder {
display: grid;
grid-template-rows: var(--builder-topbar-height) 1fr;
height: 100vh;
overflow: hidden;
}
/* ==========================================
TOPBAR
========================================== */
swp-builder-topbar {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 16px;
background: var(--color-surface);
border-bottom: 1px solid var(--color-border);
z-index: 100;
}
swp-topbar-left {
display: flex;
align-items: center;
gap: 16px;
}
swp-topbar-brand {
display: flex;
align-items: center;
gap: 10px;
font-size: 15px;
font-weight: 600;
color: var(--color-text);
}
swp-topbar-brand i {
font-size: 24px;
color: var(--color-teal);
}
swp-page-selector {
display: flex;
align-items: center;
gap: 8px;
padding: 8px 12px;
background: var(--color-background);
border-radius: 6px;
cursor: pointer;
transition: background 150ms ease;
}
swp-page-selector:hover {
background: var(--color-background-hover);
}
swp-page-selector select {
border: none;
background: transparent;
font-family: var(--font-family);
font-size: 13px;
font-weight: 500;
color: var(--color-text);
cursor: pointer;
outline: none;
}
swp-topbar-center {
display: flex;
align-items: center;
gap: 8px;
}
swp-viewport-toggle {
display: flex;
align-items: center;
background: var(--color-background);
border-radius: 6px;
overflow: hidden;
}
swp-viewport-btn {
display: flex;
align-items: center;
justify-content: center;
width: 36px;
height: 32px;
color: var(--color-text-secondary);
cursor: pointer;
transition: all 150ms ease;
}
swp-viewport-btn:hover {
background: var(--color-background-hover);
color: var(--color-text);
}
swp-viewport-btn.active {
background: var(--color-teal);
color: white;
}
swp-viewport-btn i {
font-size: 18px;
}
swp-topbar-right {
display: flex;
align-items: center;
gap: 10px;
}
swp-btn {
display: inline-flex;
align-items: center;
gap: 6px;
padding: 8px 16px;
font-family: var(--font-family);
font-size: 13px;
font-weight: 500;
border-radius: 6px;
cursor: pointer;
transition: all 150ms ease;
border: none;
}
swp-btn i {
font-size: 16px;
}
swp-btn.secondary {
background: var(--color-background);
color: var(--color-text);
}
swp-btn.secondary:hover {
background: var(--color-background-hover);
}
swp-btn.primary {
background: var(--color-teal);
color: white;
}
swp-btn.primary:hover {
background: #00796b;
}
/* ==========================================
WORKSPACE (3-column layout)
========================================== */
swp-builder-workspace {
display: grid;
grid-template-columns: var(--builder-library-width) 1fr var(--builder-settings-width);
height: calc(100vh - var(--builder-topbar-height));
overflow: hidden;
}
/* ==========================================
BLOCK LIBRARY (Left Panel)
========================================== */
swp-block-library {
display: flex;
flex-direction: column;
background: var(--color-surface);
border-right: 1px solid var(--color-border);
overflow: hidden;
}
swp-library-header {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
font-size: 13px;
font-weight: 600;
color: var(--color-text-secondary);
text-transform: uppercase;
letter-spacing: 0.5px;
border-bottom: 1px solid var(--color-border);
}
swp-library-header i {
font-size: 16px;
}
swp-block-category {
display: block;
padding: 16px;
border-bottom: 1px solid var(--color-border);
}
swp-category-title {
display: block;
font-size: 11px;
font-weight: 600;
color: var(--color-text-muted);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 12px;
}
swp-block-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 8px;
}
swp-block-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 6px;
padding: 12px 8px;
background: var(--color-background);
border: 1px solid transparent;
border-radius: 8px;
cursor: grab;
transition: all 150ms ease;
}
swp-block-item:hover {
border-color: var(--color-teal);
background: color-mix(in srgb, var(--color-teal) 8%, white);
}
swp-block-item.dragging {
opacity: 0.5;
cursor: grabbing;
}
swp-block-item i {
font-size: 24px;
color: var(--color-teal);
}
swp-block-item span {
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
text-align: center;
}
/* ==========================================
CANVAS (Center)
========================================== */
swp-builder-canvas {
display: flex;
flex-direction: column;
background: var(--color-background);
overflow: hidden;
}
swp-canvas-container {
flex: 1;
overflow-y: auto;
padding: 24px;
}
swp-canvas-page {
display: flex;
flex-direction: column;
min-height: 100%;
background: var(--color-surface);
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
overflow: hidden;
transition: max-width 300ms ease, font-family 200ms ease, color 200ms ease;
font-family: var(--theme-font, 'Poppins', sans-serif);
color: var(--theme-text, #333);
/* Override color variables for theme - makes all block styles use theme colors */
--color-text: var(--theme-text, #333);
--color-text-secondary: color-mix(in srgb, var(--theme-text, #333) 70%, transparent);
}
/* Responsive viewport simulation */
swp-builder-canvas[data-viewport="tablet"] swp-canvas-page {
max-width: 768px;
margin: 0 auto;
}
swp-builder-canvas[data-viewport="mobile"] swp-canvas-page {
max-width: 375px;
margin: 0 auto;
}
/* Empty state */
swp-canvas-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 60px 40px;
text-align: center;
color: var(--color-text-muted);
}
swp-canvas-empty i {
font-size: 48px;
margin-bottom: 16px;
opacity: 0.5;
}
swp-canvas-empty h3 {
font-size: 16px;
font-weight: 600;
color: var(--color-text-secondary);
margin-bottom: 8px;
}
swp-canvas-empty p {
font-size: 13px;
max-width: 280px;
}
/* Block wrapper */
swp-block-wrapper {
position: relative;
transition: box-shadow 150ms ease;
}
swp-block-wrapper:hover {
box-shadow: inset 0 0 0 2px var(--color-teal);
}
swp-block-wrapper[data-selected="true"] {
box-shadow: inset 0 0 0 2px var(--color-teal);
}
swp-block-wrapper[data-selected="true"]::before {
content: attr(data-block-type);
position: absolute;
top: 0;
left: 0;
padding: 4px 8px;
background: var(--color-teal);
color: white;
font-size: 10px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
z-index: 10;
}
/* Block controls */
swp-block-controls {
position: absolute;
top: 8px;
right: 8px;
display: flex;
gap: 4px;
opacity: 0;
transition: opacity 150ms ease;
z-index: 10;
}
swp-block-wrapper:hover swp-block-controls,
swp-block-wrapper[data-selected="true"] swp-block-controls {
opacity: 1;
}
swp-block-controls button {
display: flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
background: var(--color-surface);
border: 1px solid var(--color-border);
border-radius: 4px;
cursor: pointer;
color: var(--color-text-secondary);
transition: all 150ms ease;
}
swp-block-controls button:hover {
background: var(--color-background);
color: var(--color-text);
}
swp-block-controls button.delete:hover {
background: var(--color-red);
border-color: var(--color-red);
color: white;
}
/* Drop indicator */
swp-drop-indicator {
display: none;
height: 4px;
background: var(--color-teal);
border-radius: 2px;
margin: 8px 16px;
animation: pulse 1s infinite;
}
swp-drop-indicator.visible {
display: block;
}
@keyframes pulse {
0%, 100% { opacity: 0.5; }
50% { opacity: 1; }
}
/* ==========================================
SETTINGS PANEL (Right)
========================================== */
swp-settings-panel {
display: flex;
flex-direction: column;
background: var(--color-surface);
border-left: 1px solid var(--color-border);
overflow: hidden;
}
swp-settings-header {
display: flex;
align-items: center;
gap: 8px;
padding: 16px;
font-size: 14px;
font-weight: 600;
color: var(--color-text);
border-bottom: 1px solid var(--color-border);
}
swp-settings-header i {
font-size: 18px;
color: var(--color-teal);
}
swp-settings-tabs {
display: flex;
border-bottom: 1px solid var(--color-border);
}
swp-settings-tab {
flex: 1;
padding: 12px 16px;
font-size: 13px;
font-weight: 500;
color: var(--color-text-secondary);
text-align: center;
cursor: pointer;
border-bottom: 2px solid transparent;
transition: all 150ms ease;
}
swp-settings-tab:hover {
color: var(--color-text);
background: var(--color-background-alt);
}
swp-settings-tab.active {
color: var(--color-teal);
border-bottom-color: var(--color-teal);
}
swp-settings-content {
flex: 1;
overflow-y: auto;
padding: 16px;
}
swp-settings-empty {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
height: 100%;
text-align: center;
color: var(--color-text-muted);
padding: 24px;
}
swp-settings-empty i {
font-size: 32px;
margin-bottom: 12px;
opacity: 0.5;
}
swp-settings-empty p {
font-size: 13px;
}
/* ==========================================
EDIT SECTION (Established UI Pattern)
========================================== */
swp-edit-section {
display: flex;
flex-direction: column;
gap: 0;
}
swp-edit-row {
display: flex;
flex-direction: column;
gap: 6px;
padding: 12px 0;
border-bottom: 1px solid var(--color-border);
}
swp-edit-row:last-child {
border-bottom: none;
}
swp-edit-label {
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
}
.swp-edit-value,
input.swp-edit-value,
swp-edit-section input[type="text"],
swp-edit-section input[type="url"],
swp-edit-section input[type="email"] {
width: 100%;
font-size: 14px;
padding: 8px 12px;
border-radius: 4px;
background: var(--color-background-alt);
border: 1px solid transparent;
transition: all 150ms ease;
font-family: var(--font-family);
color: var(--color-text);
}
.swp-edit-value:hover,
swp-edit-section input[type="text"]:hover,
swp-edit-section input[type="url"]:hover,
swp-edit-section input[type="email"]:hover {
background: var(--color-background);
}
.swp-edit-value:focus,
swp-edit-section input[type="text"]:focus,
swp-edit-section input[type="url"]:focus,
swp-edit-section input[type="email"]:focus {
outline: none;
background: var(--color-surface);
border-color: var(--color-teal);
}
/* Edit Select */
swp-edit-select {
display: block;
}
swp-edit-select select,
swp-edit-section select {
width: 100%;
font-size: 14px;
font-family: inherit;
padding: 8px 12px;
border-radius: 4px;
background: var(--color-background-alt);
border: 1px solid transparent;
cursor: pointer;
transition: all 150ms ease;
color: var(--color-text);
}
swp-edit-select select:hover,
swp-edit-section select:hover {
background: var(--color-background);
}
swp-edit-select select:focus,
swp-edit-section select:focus {
outline: none;
background: var(--color-surface);
border-color: var(--color-teal);
}
/* Textarea */
.swp-edit-textarea,
textarea.swp-edit-textarea,
swp-edit-section textarea {
display: block;
width: 100%;
padding: 10px 12px;
font-size: 14px;
font-family: var(--font-family);
color: var(--color-text);
background: var(--color-background-alt);
border: 1px solid transparent;
border-radius: 4px;
resize: vertical;
min-height: 80px;
transition: all 150ms ease;
}
.swp-edit-textarea:hover,
swp-edit-section textarea:hover {
background: var(--color-background);
}
.swp-edit-textarea:focus,
swp-edit-section textarea:focus {
outline: none;
background: var(--color-surface);
border-color: var(--color-teal);
}
/* Color picker */
swp-color-picker-row {
display: flex;
align-items: center;
gap: 10px;
}
swp-color-picker-row input[type="color"] {
width: 40px;
height: 40px;
border: none;
padding: 0;
cursor: pointer;
background: none;
}
swp-color-picker-row input[type="color"]::-webkit-color-swatch-wrapper {
padding: 0;
}
swp-color-picker-row input[type="color"]::-webkit-color-swatch {
border: 2px solid var(--color-border);
border-radius: 6px;
}
swp-color-picker-row span {
font-size: 13px;
font-family: var(--font-mono);
color: var(--color-text-secondary);
}
/* Toggle Slider */
swp-toggle-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid var(--color-border);
}
swp-toggle-row:last-child {
border-bottom: none;
}
swp-toggle-info {
display: flex;
flex-direction: column;
gap: 2px;
}
swp-toggle-label {
font-size: 14px;
font-weight: 500;
color: var(--color-text);
}
swp-toggle-desc {
font-size: 12px;
color: var(--color-text-secondary);
}
/* Button Style Picker */
swp-button-style-picker {
display: flex;
gap: 8px;
}
swp-button-style-option {
display: flex;
align-items: center;
justify-content: center;
width: 60px;
height: 36px;
background: var(--color-background-alt);
border: 2px solid var(--color-border);
border-radius: 6px;
cursor: pointer;
transition: all 150ms ease;
}
swp-button-style-option:hover {
border-color: var(--color-teal);
}
swp-button-style-option.active {
border-color: var(--color-teal);
background: color-mix(in srgb, var(--color-teal) 10%, white);
}
swp-button-style-option span {
display: block;
width: 40px;
height: 20px;
background: var(--color-teal);
}
swp-button-style-option[data-style="rounded"] span {
border-radius: 4px;
}
swp-button-style-option[data-style="pill"] span {
border-radius: 10px;
}
swp-button-style-option[data-style="square"] span {
border-radius: 0;
}
/* Spacing Picker */
swp-spacing-picker {
display: flex;
gap: 8px;
}
swp-spacing-option {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 4px;
padding: 10px 8px;
background: var(--color-background-alt);
border: 2px solid var(--color-border);
border-radius: 6px;
cursor: pointer;
transition: all 150ms ease;
}
swp-spacing-option:hover {
border-color: var(--color-teal);
}
swp-spacing-option.active {
border-color: var(--color-teal);
background: color-mix(in srgb, var(--color-teal) 10%, white);
}
swp-spacing-option i {
font-size: 18px;
color: var(--color-text-secondary);
}
swp-spacing-option span {
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
}
swp-spacing-option.active i,
swp-spacing-option.active span {
color: var(--color-teal);
}
/* Section Title in Settings */
swp-section-title {
display: block;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-muted);
margin-bottom: 12px;
padding-top: 8px;
}
swp-section-title:first-child {
padding-top: 0;
}
/* Info hint in settings */
swp-settings-hint {
display: block;
font-size: 12px;
color: var(--color-text-muted);
margin-top: 8px;
padding: 8px 10px;
background: var(--color-background-alt);
border-radius: 4px;
}
swp-settings-hint i {
margin-right: 6px;
}
/* Color Scheme Picker */
swp-color-schemes {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 10px;
margin-bottom: 16px;
}
swp-color-scheme {
display: flex;
flex-direction: column;
gap: 6px;
padding: 10px;
background: var(--color-background-alt);
border: 2px solid var(--color-border);
border-radius: 8px;
cursor: pointer;
transition: all 150ms ease;
}
swp-color-scheme:hover {
border-color: var(--color-teal);
}
swp-color-scheme.active {
border-color: var(--color-teal);
background: color-mix(in srgb, var(--color-teal) 8%, white);
}
swp-color-scheme-colors {
display: flex;
gap: 4px;
height: 24px;
}
swp-color-scheme-colors span {
flex: 1;
border-radius: 4px;
}
swp-color-scheme-colors span:first-child {
border-radius: 4px 0 0 4px;
}
swp-color-scheme-colors span:last-child {
border-radius: 0 4px 4px 0;
}
swp-color-scheme-name {
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
text-align: center;
}
/* Library Tabs */
swp-library-tabs {
display: flex;
border-bottom: 1px solid var(--color-border);
margin-bottom: 0;
}
swp-library-tab {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
padding: 12px 8px;
font-size: 12px;
font-weight: 500;
color: var(--color-text-secondary);
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: all 150ms ease;
}
swp-library-tab i {
font-size: 16px;
}
swp-library-tab:hover {
color: var(--color-text);
background: var(--color-background-alt);
}
swp-library-tab.active {
color: var(--color-teal);
border-bottom-color: var(--color-teal);
}
swp-library-content {
display: none;
overflow-y: auto;
flex: 1;
}
swp-library-content.active {
display: flex;
flex-direction: column;
}
swp-library-content[data-content="design"] {
padding: 16px;
}
/* Font Cards */
swp-font-cards {
display: flex;
flex-direction: column;
gap: 10px;
}
swp-font-card {
display: flex;
flex-direction: column;
padding: 12px;
background: var(--color-background-alt);
border: 2px solid var(--color-border);
border-radius: 8px;
cursor: pointer;
transition: all 150ms ease;
}
swp-font-card:hover {
border-color: var(--color-teal);
}
swp-font-card.active {
border-color: var(--color-teal);
background: color-mix(in srgb, var(--color-teal) 8%, white);
}
swp-font-card-preview {
font-size: 20px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 4px;
line-height: 1.3;
}
swp-font-card-name {
font-size: 11px;
font-weight: 500;
color: var(--color-text-secondary);
}
swp-font-card[data-font="Poppins"] swp-font-card-preview {
font-family: 'Poppins', sans-serif;
}
swp-font-card[data-font="Playfair Display"] swp-font-card-preview {
font-family: 'Playfair Display', serif;
}
swp-font-card[data-font="Inter"] swp-font-card-preview {
font-family: 'Inter', sans-serif;
}
swp-font-card[data-font="Montserrat"] swp-font-card-preview {
font-family: 'Montserrat', sans-serif;
}
/* Design Section Title */
swp-design-section-title {
display: block;
font-size: 11px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
color: var(--color-text-muted);
margin-bottom: 10px;
margin-top: 20px;
}
swp-design-section-title:first-of-type {
margin-top: 0;
}
/* ==========================================
BLOCK STYLES (Preview)
========================================== */
/* Hero Block */
.block-hero {
position: relative;
display: flex;
align-items: center;
justify-content: center;
min-height: 400px;
padding: calc(60px * var(--theme-spacing, 1)) 40px;
background-size: cover;
background-position: center;
color: white;
text-align: center;
}
.block-hero::before {
content: '';
position: absolute;
inset: 0;
background: rgba(0,0,0,0.4);
}
.block-hero-content {
position: relative;
z-index: 1;
max-width: 600px;
}
.block-hero h1 {
font-size: 42px;
font-weight: 700;
margin-bottom: 16px;
line-height: 1.2;
}
.block-hero p {
font-size: 18px;
opacity: 0.9;
margin-bottom: 24px;
}
.block-hero-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 14px 28px;
background: var(--theme-primary);
color: white;
font-size: 15px;
font-weight: 600;
text-decoration: none;
border-radius: var(--theme-button-radius, 8px);
transition: background 150ms ease;
}
.block-hero-btn:hover {
background: color-mix(in srgb, var(--theme-primary) 85%, black);
}
/* About Text Block */
.block-about {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
padding: calc(60px * var(--theme-spacing, 1)) 40px;
align-items: center;
}
.block-about.image-right {
direction: rtl;
}
.block-about.image-right > * {
direction: ltr;
}
.block-about-content h2 {
font-size: 32px;
font-weight: 700;
margin-bottom: 16px;
color: var(--color-text);
}
.block-about-content p {
font-size: 15px;
color: var(--color-text-secondary);
line-height: 1.7;
}
.block-about-image {
aspect-ratio: 4/3;
background: var(--color-background);
border-radius: 12px;
overflow: hidden;
}
.block-about-image img {
width: 100%;
height: 100%;
object-fit: cover;
}
.block-about-image-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: var(--color-text-muted);
}
.block-about-image-placeholder i {
font-size: 48px;
}
/* Spacer Block */
.block-spacer {
height: 60px;
background: repeating-linear-gradient(
45deg,
transparent,
transparent 10px,
var(--color-background) 10px,
var(--color-background) 20px
);
opacity: 0.5;
}
.block-spacer.small { height: 30px; }
.block-spacer.large { height: 100px; }
/* Services Grid Block */
.block-services {
padding: calc(60px * var(--theme-spacing, 1)) 40px;
}
.block-services-header {
text-align: center;
margin-bottom: 40px;
}
.block-services-header h2 {
font-size: 32px;
font-weight: 700;
color: var(--color-text);
margin-bottom: 8px;
}
.block-services-header p {
font-size: 15px;
color: var(--color-text-secondary);
}
.block-services-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 24px;
}
.block-service-card {
padding: 24px;
background: var(--color-background);
border-radius: 12px;
transition: transform 150ms ease, box-shadow 150ms ease;
}
.block-service-card:hover {
transform: translateY(-4px);
box-shadow: 0 8px 24px rgba(0,0,0,0.1);
background: var(--theme-accent-bg-light, #f59e0b1a);
}
.block-service-card h3 {
font-size: 16px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 8px;
}
.block-service-card-meta {
display: flex;
align-items: center;
gap: 12px;
font-size: 13px;
color: var(--color-text-secondary);
margin-bottom: 12px;
}
.block-service-card-price {
font-size: 18px;
font-weight: 700;
font-family: var(--font-mono);
color: var(--theme-primary);
}
/* Team Block */
.block-team {
padding: calc(60px * var(--theme-spacing, 1)) 40px;
background: var(--color-background);
}
.block-team-header {
text-align: center;
margin-bottom: 40px;
}
.block-team-header h2 {
font-size: 32px;
font-weight: 700;
color: var(--color-text);
margin-bottom: 8px;
}
.block-team-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 24px;
}
.block-team-card {
text-align: center;
padding: 24px;
background: var(--color-surface);
border-radius: 12px;
transition: background 150ms ease;
}
.block-team-card:hover {
background: var(--theme-accent-bg-lighter, #f59e0b14);
}
.block-team-avatar {
width: 100px;
height: 100px;
margin: 0 auto 16px;
border-radius: 50%;
background: var(--theme-primary);
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
font-weight: 600;
color: white;
}
.block-team-card h3 {
font-size: 16px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 4px;
}
.block-team-card p {
font-size: 13px;
color: var(--color-text-secondary);
}
/* Contact Block */
.block-contact {
padding: calc(60px * var(--theme-spacing, 1)) 40px;
}
.block-contact-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 40px;
}
.block-contact-info h2 {
font-size: 28px;
font-weight: 700;
color: var(--color-text);
margin-bottom: 24px;
}
.block-contact-item {
display: flex;
align-items: flex-start;
gap: 12px;
margin-bottom: 20px;
}
.block-contact-item i {
font-size: 20px;
color: var(--theme-primary);
margin-top: 2px;
}
.block-contact-item-content h4 {
font-size: 14px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 2px;
}
.block-contact-item-content p {
font-size: 14px;
color: var(--color-text-secondary);
}
.block-contact-map {
background: var(--color-background);
border-radius: 12px;
min-height: 300px;
display: flex;
align-items: center;
justify-content: center;
color: var(--color-text-muted);
}
/* Opening Hours Block */
.block-hours {
padding: calc(60px * var(--theme-spacing, 1)) 40px;
background: var(--color-background);
}
.block-hours h2 {
font-size: 28px;
font-weight: 700;
color: var(--color-text);
margin-bottom: 24px;
text-align: center;
}
.block-hours-list {
max-width: 400px;
margin: 0 auto;
}
.block-hours-row {
display: flex;
justify-content: space-between;
padding: 12px 0;
border-bottom: 1px solid var(--color-border);
}
.block-hours-row:last-child {
border-bottom: none;
}
.block-hours-day {
font-weight: 500;
color: var(--color-text);
}
.block-hours-time {
font-family: var(--font-mono);
color: var(--color-text-secondary);
}
.block-hours-time.closed {
color: var(--color-red);
}
/* Booking Widget Block */
.block-booking {
padding: calc(60px * var(--theme-spacing, 1)) 40px;
text-align: center;
background: linear-gradient(135deg, var(--theme-primary) 0%, color-mix(in srgb, var(--theme-primary) 80%, black) 100%);
color: white;
}
.block-booking h2 {
font-size: 32px;
font-weight: 700;
margin-bottom: 12px;
}
.block-booking p {
font-size: 16px;
opacity: 0.9;
margin-bottom: 24px;
}
.block-booking-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 16px 32px;
background: white;
color: var(--theme-primary);
font-size: 16px;
font-weight: 600;
text-decoration: none;
border-radius: var(--theme-button-radius, 8px);
transition: transform 150ms ease;
}
.block-booking-btn:hover {
transform: scale(1.05);
}
/* ==========================================
COLUMNS BLOCK (CSS Grid)
========================================== */
.block-columns {
display: grid;
gap: 24px;
padding: calc(40px * var(--theme-spacing, 1)) 40px;
}
.block-columns[data-cols="2"] {
grid-template-columns: 1fr 1fr;
}
.block-columns[data-cols="3"] {
grid-template-columns: 1fr 1fr 1fr;
}
.block-columns[data-cols="1-2"] {
grid-template-columns: 1fr 2fr;
}
.block-columns[data-cols="2-1"] {
grid-template-columns: 2fr 1fr;
}
.block-column {
padding: 24px;
background: var(--color-background-alt);
border-radius: 8px;
min-height: 120px;
}
.block-column h3 {
font-size: 20px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 12px;
}
.block-column p {
font-size: 14px;
color: var(--color-text-secondary);
line-height: 1.6;
}
.block-column-placeholder {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
min-height: 80px;
color: var(--color-text-muted);
font-size: 13px;
border: 2px dashed var(--color-border);
border-radius: 6px;
}
/* Divider Block */
.block-divider {
padding: 20px 40px;
}
.block-divider hr {
border: none;
height: 1px;
background: var(--color-border);
}
.block-divider.thick hr {
height: 3px;
}
.block-divider.dashed hr {
background: none;
border-top: 2px dashed var(--color-border);
}
/* Gallery Block */
.block-gallery {
padding: calc(60px * var(--theme-spacing, 1)) 40px;
}
.block-gallery-header {
text-align: center;
margin-bottom: 32px;
}
.block-gallery-header h2 {
font-size: 28px;
font-weight: 700;
color: var(--color-text);
}
.block-gallery-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 16px;
}
.block-gallery-item {
aspect-ratio: 1;
background: var(--color-background);
border-radius: 8px;
overflow: hidden;
}
.block-gallery-item img {
width: 100%;
height: 100%;
object-fit: cover;
}
.block-gallery-placeholder {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: var(--color-text-muted);
}
.block-gallery-placeholder i {
font-size: 32px;
}
/* Testimonials Block */
.block-testimonials {
padding: calc(60px * var(--theme-spacing, 1)) 40px;
background: var(--color-background);
}
.block-testimonials-header {
text-align: center;
margin-bottom: 40px;
}
.block-testimonials-header h2 {
font-size: 28px;
font-weight: 700;
color: var(--color-text);
}
.block-testimonials-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
}
.block-testimonial-card {
padding: 24px;
background: var(--color-surface);
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
}
.block-testimonial-stars {
color: var(--theme-accent);
margin-bottom: 12px;
}
.block-testimonial-text {
font-size: 15px;
color: var(--color-text);
line-height: 1.6;
margin-bottom: 16px;
font-style: italic;
}
.block-testimonial-author {
display: flex;
align-items: center;
gap: 12px;
}
.block-testimonial-avatar {
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--theme-primary);
display: flex;
align-items: center;
justify-content: center;
color: white;
font-weight: 600;
font-size: 14px;
}
.block-testimonial-name {
font-size: 14px;
font-weight: 600;
color: var(--color-text);
}
/* CTA Banner Block */
.block-cta {
padding: calc(50px * var(--theme-spacing, 1)) 40px;
background: var(--theme-primary);
text-align: center;
color: white;
}
.block-cta h2 {
font-size: 28px;
font-weight: 700;
margin-bottom: 12px;
}
.block-cta p {
font-size: 16px;
opacity: 0.9;
margin-bottom: 20px;
}
.block-cta-btn {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 14px 28px;
background: white;
color: var(--theme-primary);
font-size: 15px;
font-weight: 600;
text-decoration: none;
border-radius: var(--theme-button-radius, 8px);
transition: transform 150ms ease;
}
.block-cta-btn:hover {
transform: scale(1.05);
}
/* Prices Block */
.block-prices {
padding: calc(60px * var(--theme-spacing, 1)) 40px;
}
.block-prices-header {
text-align: center;
margin-bottom: 40px;
}
.block-prices-header h2 {
font-size: 28px;
font-weight: 700;
color: var(--color-text);
}
.block-prices-list {
max-width: 600px;
margin: 0 auto;
}
.block-price-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px 0;
border-bottom: 1px solid var(--color-border);
}
.block-price-item:last-child {
border-bottom: none;
}
.block-price-name {
font-size: 15px;
font-weight: 500;
color: var(--color-text);
}
.block-price-value {
font-size: 16px;
font-weight: 700;
font-family: var(--font-mono);
color: var(--theme-primary);
}
/* FAQ Block */
.block-faq {
padding: calc(60px * var(--theme-spacing, 1)) 40px;
background: var(--color-background);
}
.block-faq-header {
text-align: center;
margin-bottom: 40px;
}
.block-faq-header h2 {
font-size: 28px;
font-weight: 700;
color: var(--color-text);
}
.block-faq-list {
max-width: 700px;
margin: 0 auto;
}
.block-faq-item {
background: var(--color-surface);
border-radius: 8px;
margin-bottom: 12px;
overflow: hidden;
border-left: 3px solid var(--theme-accent);
}
.block-faq-question {
display: flex;
align-items: center;
justify-content: space-between;
padding: 16px 20px;
font-size: 15px;
font-weight: 600;
color: var(--color-text);
cursor: pointer;
}
.block-faq-question i {
color: var(--color-text-secondary);
}
.block-faq-answer {
padding: 0 20px 16px;
font-size: 14px;
color: var(--color-text-secondary);
line-height: 1.6;
}
/* Map Block */
.block-map {
height: 400px;
background: var(--color-background);
display: flex;
align-items: center;
justify-content: center;
}
.block-map-placeholder {
text-align: center;
color: var(--color-text-muted);
}
.block-map-placeholder i {
font-size: 64px;
margin-bottom: 16px;
}
.block-map-placeholder p {
font-size: 14px;
}
/* Social Block */
.block-social {
padding: calc(40px * var(--theme-spacing, 1)) 40px;
text-align: center;
}
.block-social h3 {
font-size: 18px;
font-weight: 600;
color: var(--color-text);
margin-bottom: 20px;
}
.block-social-links {
display: flex;
justify-content: center;
gap: 16px;
flex-wrap: wrap;
}
.block-social-link {
display: flex;
align-items: center;
justify-content: center;
width: 48px;
height: 48px;
background: var(--theme-primary);
color: white;
border-radius: 50%;
font-size: 22px;
text-decoration: none;
transition: transform 150ms ease, background 150ms ease;
}
.block-social-link:hover {
transform: scale(1.1);
background: color-mix(in srgb, var(--theme-primary) 85%, black);
}
.block-social-link.style-outline {
background: transparent;
border: 2px solid var(--theme-primary);
color: var(--theme-primary);
}
.block-social-link.style-outline:hover {
background: var(--theme-primary);
color: white;
}
.block-social-link.style-square {
border-radius: 8px;
}
</style>
</head>
<body>
<swp-website-builder>
<!-- Topbar -->
<swp-builder-topbar>
<swp-topbar-left>
<swp-topbar-brand>
<i class="ph ph-browsers"></i>
Website Builder
</swp-topbar-brand>
<swp-page-selector>
<i class="ph ph-file-text"></i>
<select id="pageSelector">
<option value="home">Forside</option>
<option value="services">Ydelser</option>
<option value="team">Team</option>
<option value="about">Om os</option>
</select>
</swp-page-selector>
</swp-topbar-left>
<swp-topbar-center>
<swp-viewport-toggle>
<swp-viewport-btn class="active" data-viewport="desktop" title="Desktop">
<i class="ph ph-desktop"></i>
</swp-viewport-btn>
<swp-viewport-btn data-viewport="tablet" title="Tablet">
<i class="ph ph-device-tablet"></i>
</swp-viewport-btn>
<swp-viewport-btn data-viewport="mobile" title="Mobil">
<i class="ph ph-device-mobile"></i>
</swp-viewport-btn>
</swp-viewport-toggle>
</swp-topbar-center>
<swp-topbar-right>
<swp-btn class="secondary" id="previewBtn">
<i class="ph ph-eye"></i>
Preview
</swp-btn>
<swp-btn class="secondary" id="saveBtn">
<i class="ph ph-floppy-disk"></i>
Gem
</swp-btn>
<swp-btn class="primary" id="publishBtn">
<i class="ph ph-rocket-launch"></i>
Publicer
</swp-btn>
</swp-topbar-right>
</swp-builder-topbar>
<!-- Workspace -->
<swp-builder-workspace>
<!-- Block Library -->
<swp-block-library>
<swp-library-tabs>
<swp-library-tab class="active" data-tab="blocks">
<i class="ph ph-squares-four"></i>
Blokke
</swp-library-tab>
<swp-library-tab data-tab="design">
<i class="ph ph-palette"></i>
Design
</swp-library-tab>
</swp-library-tabs>
<!-- Blokke Tab -->
<swp-library-content class="active" data-content="blocks">
<swp-block-category>
<swp-category-title>Layout</swp-category-title>
<swp-block-grid>
<swp-block-item draggable="true" data-block-type="hero">
<i class="ph ph-image"></i>
<span>Hero</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="columns">
<i class="ph ph-columns"></i>
<span>Kolonner</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="spacer">
<i class="ph ph-arrows-out-line-vertical"></i>
<span>Spacer</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="divider">
<i class="ph ph-minus"></i>
<span>Divider</span>
</swp-block-item>
</swp-block-grid>
</swp-block-category>
<swp-block-category>
<swp-category-title>Indhold</swp-category-title>
<swp-block-grid>
<swp-block-item draggable="true" data-block-type="about">
<i class="ph ph-article"></i>
<span>Tekst</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="services">
<i class="ph ph-scissors"></i>
<span>Ydelser</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="team">
<i class="ph ph-users-three"></i>
<span>Team</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="gallery">
<i class="ph ph-images"></i>
<span>Galleri</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="testimonials">
<i class="ph ph-quotes"></i>
<span>Anmeldelser</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="cta">
<i class="ph ph-megaphone"></i>
<span>CTA Banner</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="prices">
<i class="ph ph-currency-circle-dollar"></i>
<span>Prisliste</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="faq">
<i class="ph ph-question"></i>
<span>FAQ</span>
</swp-block-item>
</swp-block-grid>
</swp-block-category>
<swp-block-category>
<swp-category-title>Kontakt & Booking</swp-category-title>
<swp-block-grid>
<swp-block-item draggable="true" data-block-type="contact">
<i class="ph ph-map-pin"></i>
<span>Kontakt</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="hours">
<i class="ph ph-clock"></i>
<span>Åbningstider</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="booking">
<i class="ph ph-calendar-check"></i>
<span>Book tid</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="map">
<i class="ph ph-map-trifold"></i>
<span>Kort</span>
</swp-block-item>
<swp-block-item draggable="true" data-block-type="social">
<i class="ph ph-share-network"></i>
<span>Social</span>
</swp-block-item>
</swp-block-grid>
</swp-block-category>
</swp-library-content>
<!-- Design Tab -->
<swp-library-content data-content="design">
<swp-design-section-title>Farveskema</swp-design-section-title>
<swp-color-schemes id="libraryColorSchemes">
<!-- Color schemes will be rendered here -->
</swp-color-schemes>
<swp-design-section-title>Typografi</swp-design-section-title>
<swp-font-cards id="libraryFontCards">
<swp-font-card data-font="Poppins">
<swp-font-card-preview>Velkommen til salonen</swp-font-card-preview>
<swp-font-card-name>Poppins — Moderne & venlig</swp-font-card-name>
</swp-font-card>
<swp-font-card data-font="Playfair Display">
<swp-font-card-preview>Velkommen til salonen</swp-font-card-preview>
<swp-font-card-name>Playfair Display — Elegant & klassisk</swp-font-card-name>
</swp-font-card>
<swp-font-card data-font="Inter">
<swp-font-card-preview>Velkommen til salonen</swp-font-card-preview>
<swp-font-card-name>Inter — Ren & professionel</swp-font-card-name>
</swp-font-card>
<swp-font-card data-font="Montserrat">
<swp-font-card-preview>Velkommen til salonen</swp-font-card-preview>
<swp-font-card-name>Montserrat — Bold & stilfuld</swp-font-card-name>
</swp-font-card>
</swp-font-cards>
<swp-design-section-title>Sociale medier</swp-design-section-title>
<swp-edit-section id="socialLinksSettings">
<swp-edit-row>
<swp-edit-label><i class="ph ph-instagram-logo"></i> Instagram</swp-edit-label>
<input type="url" class="swp-edit-value" data-social="instagram" placeholder="https://instagram.com/...">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label><i class="ph ph-facebook-logo"></i> Facebook</swp-edit-label>
<input type="url" class="swp-edit-value" data-social="facebook" placeholder="https://facebook.com/...">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label><i class="ph ph-tiktok-logo"></i> TikTok</swp-edit-label>
<input type="url" class="swp-edit-value" data-social="tiktok" placeholder="https://tiktok.com/@...">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label><i class="ph ph-linkedin-logo"></i> LinkedIn</swp-edit-label>
<input type="url" class="swp-edit-value" data-social="linkedin" placeholder="https://linkedin.com/...">
</swp-edit-row>
</swp-edit-section>
</swp-library-content>
</swp-block-library>
<!-- Canvas -->
<swp-builder-canvas data-viewport="desktop">
<swp-canvas-container>
<swp-canvas-page id="canvasPage">
<!-- Blocks will be rendered here -->
</swp-canvas-page>
</swp-canvas-container>
</swp-builder-canvas>
<!-- Settings Panel -->
<swp-settings-panel>
<swp-settings-header>
<i class="ph ph-sliders"></i>
<span id="settingsTitle">Indstillinger</span>
</swp-settings-header>
<swp-settings-tabs>
<swp-settings-tab class="active" data-tab="content">Indhold</swp-settings-tab>
<swp-settings-tab data-tab="style">Styling</swp-settings-tab>
</swp-settings-tabs>
<swp-settings-content id="settingsContent">
<swp-settings-empty>
<i class="ph ph-cursor-click"></i>
<p>Vælg en blok for at redigere</p>
</swp-settings-empty>
</swp-settings-content>
</swp-settings-panel>
</swp-builder-workspace>
</swp-website-builder>
<script>
// ==========================================
// INDEXEDDB DATABASE
// ==========================================
class WebsiteBuilderDB {
constructor() {
this.dbName = 'WebsiteBuilderDB';
this.dbVersion = 1;
this.storeName = 'sites';
this.db = null;
}
async init() {
return new Promise((resolve, reject) => {
const request = indexedDB.open(this.dbName, this.dbVersion);
request.onerror = () => reject(request.error);
request.onsuccess = () => {
this.db = request.result;
resolve(this.db);
};
request.onupgradeneeded = (event) => {
const db = event.target.result;
if (!db.objectStoreNames.contains(this.storeName)) {
db.createObjectStore(this.storeName, { keyPath: 'id' });
}
};
});
}
async get(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.get(id);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
async put(data) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.put(data);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
async delete(id) {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readwrite');
const store = transaction.objectStore(this.storeName);
const request = store.delete(id);
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
async getAll() {
return new Promise((resolve, reject) => {
const transaction = this.db.transaction([this.storeName], 'readonly');
const store = transaction.objectStore(this.storeName);
const request = store.getAll();
request.onerror = () => reject(request.error);
request.onsuccess = () => resolve(request.result);
});
}
}
// ==========================================
// COLOR SCHEMES 2026
// ==========================================
const colorSchemes = [
{
id: 'mocha-mousse',
name: 'Mocha Mousse',
colors: { primary: '#A47864', secondary: '#5C3D2E', accent: '#D4A574', background: '#F5EDE8' }
},
{
id: 'ocean-teal',
name: 'Ocean Teal',
colors: { primary: '#0D7377', secondary: '#14403B', accent: '#32E0C4', background: '#E8F6F3' }
},
{
id: 'sunset-orange',
name: 'Warm Sunset',
colors: { primary: '#FF6633', secondary: '#402010', accent: '#FFAA55', background: '#FFF5F0' }
},
{
id: 'mint-wellness',
name: 'Mint Wellness',
colors: { primary: '#4ECDC4', secondary: '#1A535C', accent: '#95E1D3', background: '#F0FAF9' }
},
{
id: 'midnight-blue',
name: 'Midnight Blue',
colors: { primary: '#4D7FFF', secondary: '#1A2540', accent: '#FFCC00', background: '#F0F4FF' }
},
{
id: 'dusty-rose',
name: 'Dusty Rose',
colors: { primary: '#B84D77', secondary: '#4A1942', accent: '#F0E6D9', background: '#FDF5F7' }
},
{
id: 'emerald-gold',
name: 'Emerald & Gold',
colors: { primary: '#2D8566', secondary: '#1A3D2E', accent: '#E6994D', background: '#F0F7F4' }
},
{
id: 'retro-70s',
name: 'Retro 70\'s',
colors: { primary: '#E07B39', secondary: '#101357', accent: '#FBB13C', background: '#FEF9F3' }
},
{
id: 'nordic-calm',
name: 'Nordic Calm',
colors: { primary: '#6B7A8F', secondary: '#2C3E50', accent: '#A8B5C4', background: '#F5F7FA' }
},
{
id: 'salon-classic',
name: 'Salon Classic',
colors: { primary: '#00897b', secondary: '#333333', accent: '#f59e0b', background: '#ffffff' }
}
];
// ==========================================
// STATE MANAGEMENT
// ==========================================
const defaultSite = {
id: 'site-001',
name: 'KARINA KNUDSEN',
theme: {
primaryColor: '#00897b',
secondaryColor: '#333333',
fontFamily: 'Poppins',
colorScheme: 'salon-classic'
},
socialLinks: {
instagram: '',
facebook: '',
tiktok: '',
linkedin: ''
},
pages: [
{
id: 'home',
slug: '/',
title: 'Forside',
isHomepage: true,
blocks: [
{
id: 'block-1',
type: 'hero',
content: {
headline: 'Velkommen til KARINA KNUDSEN',
subheadline: 'Din frisør på Amager siden 2010',
backgroundImage: 'https://images.unsplash.com/photo-1560066984-138dadb4c035?w=1200',
ctaText: 'Book tid',
ctaLink: '#booking'
}
},
{
id: 'block-2',
type: 'about',
content: {
title: 'Om salonen',
text: 'Vi er en moderne frisørsalon med fokus på kvalitet og personlig service. Vores dygtige stylister holder sig altid opdateret med de nyeste trends og teknikker.',
imageUrl: 'https://images.unsplash.com/photo-1522337360788-8b13dee7a37e?w=800',
imagePosition: 'right'
}
},
{
id: 'block-3',
type: 'services',
content: {
title: 'Vores ydelser',
subtitle: 'Vi tilbyder et bredt udvalg af behandlinger'
}
},
{
id: 'block-4',
type: 'booking',
content: {
title: 'Book din tid',
subtitle: 'Vælg en tid der passer dig',
buttonText: 'Book nu'
}
}
]
},
{ id: 'services', slug: '/ydelser', title: 'Ydelser', blocks: [] },
{ id: 'team', slug: '/team', title: 'Team', blocks: [] },
{ id: 'about', slug: '/om-os', title: 'Om os', blocks: [] }
]
};
// Services data (simulated from kk-services.json)
const servicesData = [
{ name: 'Dameklip', duration: 60, price: 725, category: 'Klip' },
{ name: 'Herreklip', duration: 60, price: 645, category: 'Klip' },
{ name: 'Børneklip', duration: 45, price: 475, category: 'Klip' },
{ name: 'Bundfarve', duration: 90, price: 785, category: 'Farve' },
{ name: 'Striber', duration: 120, price: 1465, category: 'Farve' },
{ name: 'Balayage', duration: 150, price: 1850, category: 'Farve' }
];
// Team data (simulated from mock-resources.json)
const teamData = [
{ id: 'EMP001', name: 'Camilla', role: 'Master Stylist', color: '#9c27b0' },
{ id: 'EMP002', name: 'Isabella', role: 'Master Stylist', color: '#e91e63' },
{ id: 'EMP003', name: 'Alexander', role: 'Frisør', color: '#3f51b5' },
{ id: 'EMP004', name: 'Viktor', role: 'Junior Stylist', color: '#009688' }
];
class WebsiteBuilderState {
constructor() {
this.db = new WebsiteBuilderDB();
this.site = null;
this.currentPageId = 'home';
this.selectedBlockId = null;
this.isInitialized = false;
}
async initialize() {
await this.db.init();
this.site = await this.loadSite();
this.isInitialized = true;
this.setupEventListeners();
this.render(); // render() calls applyTheme() after renderCanvas()
}
async loadSite() {
const saved = await this.db.get('site-001');
return saved || JSON.parse(JSON.stringify(defaultSite));
}
async saveSite() {
await this.db.put(this.site);
}
getCurrentPage() {
return this.site.pages.find(p => p.id === this.currentPageId);
}
getSelectedBlock() {
if (!this.selectedBlockId) return null;
const page = this.getCurrentPage();
return page.blocks.find(b => b.id === this.selectedBlockId);
}
async addBlock(type, index) {
const page = this.getCurrentPage();
const newBlock = {
id: `block-${Date.now()}`,
type,
content: this.getDefaultContent(type)
};
if (index !== undefined) {
page.blocks.splice(index, 0, newBlock);
} else {
page.blocks.push(newBlock);
}
this.selectedBlockId = newBlock.id;
await this.saveSite();
this.render();
}
getDefaultContent(type) {
const defaults = {
hero: {
headline: 'Overskrift her',
subheadline: 'Underoverskrift her',
backgroundImage: 'https://images.unsplash.com/photo-1560066984-138dadb4c035?w=1200',
ctaText: 'Læs mere',
ctaLink: '#'
},
columns: {
layout: '2', // '2', '3', '1-2', '2-1'
column1Title: 'Kolonne 1',
column1Text: 'Indhold til første kolonne...',
column2Title: 'Kolonne 2',
column2Text: 'Indhold til anden kolonne...',
column3Title: 'Kolonne 3',
column3Text: 'Indhold til tredje kolonne...'
},
about: {
title: 'Om os',
text: 'Skriv noget om din salon her...',
imageUrl: '',
imagePosition: 'right'
},
spacer: {
size: 'medium'
},
divider: {
style: 'normal' // 'normal', 'thick', 'dashed'
},
services: {
title: 'Vores ydelser',
subtitle: 'Se vores behandlinger'
},
team: {
title: 'Mød teamet',
subtitle: 'Vores dygtige medarbejdere'
},
gallery: {
title: 'Galleri',
images: [
'https://images.unsplash.com/photo-1560066984-138dadb4c035?w=400',
'https://images.unsplash.com/photo-1522337360788-8b13dee7a37e?w=400',
'https://images.unsplash.com/photo-1595476108010-b4d1f102b1b1?w=400',
'',
'',
''
]
},
testimonials: {
title: 'Hvad siger vores kunder',
items: [
{ text: 'Fantastisk service og super dygtige stylister!', author: 'Maria J.', rating: 5 },
{ text: 'Bedste frisør på Amager. Kommer her hver gang.', author: 'Thomas K.', rating: 5 }
]
},
cta: {
title: 'Klar til forandring?',
text: 'Book en tid i dag og oplev forskellen',
buttonText: 'Book nu',
buttonLink: '#booking'
},
prices: {
title: 'Priser',
items: [
{ name: 'Dameklip', price: '725 kr' },
{ name: 'Herreklip', price: '645 kr' },
{ name: 'Børneklip', price: '475 kr' },
{ name: 'Farve', price: 'fra 785 kr' }
]
},
faq: {
title: 'Ofte stillede spørgsmål',
items: [
{ question: 'Skal jeg bestille tid?', answer: 'Ja, vi anbefaler at booke tid på forhånd for at sikre dig en plads.' },
{ question: 'Hvor lang tid tager en klipning?', answer: 'En dameklip tager typisk 45-60 minutter, herreklip ca. 30 minutter.' }
]
},
contact: {
title: 'Kontakt os',
address: 'Amager Strandvej 22f, 2300 Kbh S',
phone: '+45 32 54 00 00',
email: 'info@karinaknudsen.dk'
},
hours: {
title: 'Åbningstider',
hours: [
{ day: 'Mandag', time: '09:00 - 17:00' },
{ day: 'Tirsdag', time: '09:00 - 17:00' },
{ day: 'Onsdag', time: '09:00 - 17:00' },
{ day: 'Torsdag', time: '09:00 - 17:00' },
{ day: 'Fredag', time: '09:00 - 17:00' },
{ day: 'Lørdag', time: 'Lukket' },
{ day: 'Søndag', time: 'Lukket' }
]
},
booking: {
title: 'Book din tid',
subtitle: 'Vælg en tid der passer dig',
buttonText: 'Book nu'
},
map: {
address: 'Amager Strandvej 22f, 2300 København S'
},
social: {
title: 'Følg os',
style: 'filled' // 'filled', 'outline'
}
};
return defaults[type] || {};
}
async updateBlock(blockId, updates) {
const page = this.getCurrentPage();
const block = page.blocks.find(b => b.id === blockId);
if (block) {
Object.assign(block.content, updates);
await this.saveSite();
this.render();
}
}
async deleteBlock(blockId) {
const page = this.getCurrentPage();
const index = page.blocks.findIndex(b => b.id === blockId);
if (index > -1) {
page.blocks.splice(index, 1);
if (this.selectedBlockId === blockId) {
this.selectedBlockId = null;
}
await this.saveSite();
this.render();
}
}
async moveBlock(blockId, newIndex) {
const page = this.getCurrentPage();
const oldIndex = page.blocks.findIndex(b => b.id === blockId);
if (oldIndex > -1 && oldIndex !== newIndex) {
const [block] = page.blocks.splice(oldIndex, 1);
page.blocks.splice(newIndex > oldIndex ? newIndex - 1 : newIndex, 0, block);
await this.saveSite();
this.render();
}
}
selectBlock(blockId) {
this.selectedBlockId = blockId;
this.render();
}
setupEventListeners() {
// Page selector
document.getElementById('pageSelector').addEventListener('change', (e) => {
this.currentPageId = e.target.value;
this.selectedBlockId = null;
this.render();
});
// Viewport toggle
document.querySelectorAll('swp-viewport-btn').forEach(btn => {
btn.addEventListener('click', () => {
document.querySelectorAll('swp-viewport-btn').forEach(b => b.classList.remove('active'));
btn.classList.add('active');
document.querySelector('swp-builder-canvas').dataset.viewport = btn.dataset.viewport;
});
});
// Settings tabs
document.querySelectorAll('swp-settings-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('swp-settings-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
this.renderSettings();
});
});
// Library tabs (Blokke | Design)
document.querySelectorAll('swp-library-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('swp-library-tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('swp-library-content').forEach(c => c.classList.remove('active'));
tab.classList.add('active');
const content = document.querySelector(`swp-library-content[data-content="${tab.dataset.tab}"]`);
if (content) content.classList.add('active');
});
});
// Color scheme selection in library
document.getElementById('libraryColorSchemes').addEventListener('click', async (e) => {
const scheme = e.target.closest('swp-color-scheme');
if (scheme) {
const schemeId = scheme.dataset.scheme;
const schemeData = colorSchemes.find(s => s.id === schemeId);
if (schemeData) {
this.site.theme.colorScheme = schemeId;
this.site.theme.primaryColor = schemeData.colors.primary;
this.site.theme.secondaryColor = schemeData.colors.secondary;
this.site.theme.accentColor = schemeData.colors.accent;
await this.saveSite();
this.renderCanvas();
this.applyTheme(); // Must run AFTER renderCanvas
this.renderLibraryDesign();
this.renderSettings();
}
}
});
// Font selection in library
document.getElementById('libraryFontCards').addEventListener('click', async (e) => {
const card = e.target.closest('swp-font-card');
if (card) {
const fontFamily = card.dataset.font;
this.site.theme.fontFamily = fontFamily;
await this.saveSite();
this.renderCanvas();
this.applyTheme(); // Must run AFTER renderCanvas
this.renderLibraryDesign();
this.renderSettings();
}
});
// Social links settings
document.getElementById('socialLinksSettings').addEventListener('input', async (e) => {
const input = e.target;
if (input.dataset.social) {
if (!this.site.socialLinks) this.site.socialLinks = {};
this.site.socialLinks[input.dataset.social] = input.value;
await this.saveSite();
this.renderCanvas();
}
});
// Save button
document.getElementById('saveBtn').addEventListener('click', async () => {
await this.saveSite();
alert('Gemt!');
});
// Preview button
document.getElementById('previewBtn').addEventListener('click', () => {
window.open('poc-website-preview.html', '_blank');
});
// Drag and drop
this.setupDragAndDrop();
}
setupDragAndDrop() {
const canvas = document.getElementById('canvasPage');
// Block library items
document.querySelectorAll('swp-block-item').forEach(item => {
item.addEventListener('dragstart', (e) => {
e.dataTransfer.setData('application/json', JSON.stringify({
action: 'add',
type: item.dataset.blockType
}));
e.dataTransfer.effectAllowed = 'copy';
item.classList.add('dragging');
});
item.addEventListener('dragend', () => {
item.classList.remove('dragging');
this.hideDropIndicators();
});
});
// Canvas drop zone
canvas.addEventListener('dragover', (e) => {
e.preventDefault();
e.dataTransfer.dropEffect = 'copy';
this.showDropIndicator(e.clientY);
});
canvas.addEventListener('dragleave', () => {
this.hideDropIndicators();
});
canvas.addEventListener('drop', async (e) => {
e.preventDefault();
const data = JSON.parse(e.dataTransfer.getData('application/json'));
const dropIndex = this.calculateDropIndex(e.clientY);
if (data.action === 'add') {
await this.addBlock(data.type, dropIndex);
}
this.hideDropIndicators();
});
}
calculateDropIndex(clientY) {
const blocks = document.querySelectorAll('swp-block-wrapper');
for (let i = 0; i < blocks.length; i++) {
const rect = blocks[i].getBoundingClientRect();
if (clientY < rect.top + rect.height / 2) {
return i;
}
}
return blocks.length;
}
showDropIndicator(clientY) {
this.hideDropIndicators();
const index = this.calculateDropIndex(clientY);
const blocks = document.querySelectorAll('swp-block-wrapper');
const indicator = document.createElement('swp-drop-indicator');
indicator.classList.add('visible');
if (blocks.length === 0) {
document.getElementById('canvasPage').appendChild(indicator);
} else if (index >= blocks.length) {
blocks[blocks.length - 1].after(indicator);
} else {
blocks[index].before(indicator);
}
}
hideDropIndicators() {
document.querySelectorAll('swp-drop-indicator').forEach(el => el.remove());
}
render() {
this.renderCanvas();
this.applyTheme(); // Must run AFTER renderCanvas
this.renderSettings();
this.renderLibraryDesign();
}
renderCanvas() {
const canvas = document.getElementById('canvasPage');
const page = this.getCurrentPage();
if (!page.blocks.length) {
canvas.innerHTML = `
<swp-canvas-empty>
<i class="ph ph-plus-circle"></i>
<h3>Tilføj din første blok</h3>
<p>Træk en blok fra biblioteket til venstre og slip den her</p>
</swp-canvas-empty>
`;
return;
}
canvas.innerHTML = page.blocks.map(block => this.renderBlock(block)).join('');
// Add click handlers
canvas.querySelectorAll('swp-block-wrapper').forEach(wrapper => {
wrapper.addEventListener('click', (e) => {
if (!e.target.closest('swp-block-controls')) {
this.selectBlock(wrapper.dataset.blockId);
}
});
});
// Add delete handlers
canvas.querySelectorAll('.delete-block').forEach(btn => {
btn.addEventListener('click', async () => {
if (confirm('Slet denne blok?')) {
await this.deleteBlock(btn.dataset.blockId);
}
});
});
}
renderBlock(block) {
const isSelected = block.id === this.selectedBlockId;
const blockHtml = this.getBlockHtml(block);
return `
<swp-block-wrapper
data-block-id="${block.id}"
data-block-type="${block.type}"
data-selected="${isSelected}"
>
<swp-block-controls>
<button class="delete-block delete" data-block-id="${block.id}" title="Slet">
<i class="ph ph-trash"></i>
</button>
</swp-block-controls>
${blockHtml}
</swp-block-wrapper>
`;
}
getBlockHtml(block) {
const renderers = {
hero: (b) => `
<div class="block-hero" style="background-image: url('${b.content.backgroundImage}')">
<div class="block-hero-content">
<h1>${b.content.headline}</h1>
<p>${b.content.subheadline}</p>
<a href="${b.content.ctaLink}" class="block-hero-btn">
${b.content.ctaText}
<i class="ph ph-arrow-right"></i>
</a>
</div>
</div>
`,
about: (b) => `
<div class="block-about ${b.content.imagePosition === 'left' ? 'image-left' : 'image-right'}">
<div class="block-about-content">
<h2>${b.content.title}</h2>
<p>${b.content.text}</p>
</div>
<div class="block-about-image">
${b.content.imageUrl
? `<img src="${b.content.imageUrl}" alt="${b.content.title}">`
: `<div class="block-about-image-placeholder"><i class="ph ph-image"></i></div>`
}
</div>
</div>
`,
spacer: (b) => `
<div class="block-spacer ${b.content.size}"></div>
`,
services: (b) => `
<div class="block-services">
<div class="block-services-header">
<h2>${b.content.title}</h2>
<p>${b.content.subtitle}</p>
</div>
<div class="block-services-grid">
${servicesData.slice(0, 6).map(s => `
<div class="block-service-card">
<h3>${s.name}</h3>
<div class="block-service-card-meta">
<span><i class="ph ph-clock"></i> ${s.duration} min</span>
</div>
<div class="block-service-card-price">${s.price} kr.</div>
</div>
`).join('')}
</div>
</div>
`,
team: (b) => `
<div class="block-team">
<div class="block-team-header">
<h2>${b.content.title || 'Mød teamet'}</h2>
</div>
<div class="block-team-grid">
${teamData.map(t => `
<div class="block-team-card">
<div class="block-team-avatar" style="background: ${t.color}">${t.name[0]}</div>
<h3>${t.name}</h3>
<p>${t.role}</p>
</div>
`).join('')}
</div>
</div>
`,
contact: (b) => `
<div class="block-contact">
<div class="block-contact-grid">
<div class="block-contact-info">
<h2>${b.content.title}</h2>
<div class="block-contact-item">
<i class="ph ph-map-pin"></i>
<div class="block-contact-item-content">
<h4>Adresse</h4>
<p>${b.content.address}</p>
</div>
</div>
<div class="block-contact-item">
<i class="ph ph-phone"></i>
<div class="block-contact-item-content">
<h4>Telefon</h4>
<p>${b.content.phone}</p>
</div>
</div>
<div class="block-contact-item">
<i class="ph ph-envelope"></i>
<div class="block-contact-item-content">
<h4>Email</h4>
<p>${b.content.email}</p>
</div>
</div>
</div>
<div class="block-contact-map">
<i class="ph ph-map-trifold" style="font-size: 48px;"></i>
</div>
</div>
</div>
`,
hours: (b) => `
<div class="block-hours">
<h2>${b.content.title}</h2>
<div class="block-hours-list">
${b.content.hours.map(h => `
<div class="block-hours-row">
<span class="block-hours-day">${h.day}</span>
<span class="block-hours-time ${h.time === 'Lukket' ? 'closed' : ''}">${h.time}</span>
</div>
`).join('')}
</div>
</div>
`,
booking: (b) => `
<div class="block-booking">
<h2>${b.content.title}</h2>
<p>${b.content.subtitle}</p>
<a href="poc-booking-v2.html" target="_blank" class="block-booking-btn">
<i class="ph ph-calendar-check"></i>
${b.content.buttonText}
</a>
</div>
`,
columns: (b) => {
const cols = b.content.layout || '2';
const showCol3 = cols === '3';
return `
<div class="block-columns" data-cols="${cols}">
<div class="block-column">
${b.content.column1Title || b.content.column1Text
? `<h3>${b.content.column1Title || ''}</h3><p>${b.content.column1Text || ''}</p>`
: `<div class="block-column-placeholder">Kolonne 1</div>`
}
</div>
<div class="block-column">
${b.content.column2Title || b.content.column2Text
? `<h3>${b.content.column2Title || ''}</h3><p>${b.content.column2Text || ''}</p>`
: `<div class="block-column-placeholder">Kolonne 2</div>`
}
</div>
${showCol3 ? `
<div class="block-column">
${b.content.column3Title || b.content.column3Text
? `<h3>${b.content.column3Title || ''}</h3><p>${b.content.column3Text || ''}</p>`
: `<div class="block-column-placeholder">Kolonne 3</div>`
}
</div>
` : ''}
</div>
`;
},
divider: (b) => `
<div class="block-divider ${b.content.style || ''}">
<hr>
</div>
`,
gallery: (b) => `
<div class="block-gallery">
<div class="block-gallery-header">
<h2>${b.content.title}</h2>
</div>
<div class="block-gallery-grid">
${(b.content.images || []).slice(0, 6).map(img => `
<div class="block-gallery-item">
${img
? `<img src="${img}" alt="Galleri billede">`
: `<div class="block-gallery-placeholder"><i class="ph ph-image"></i></div>`
}
</div>
`).join('')}
</div>
</div>
`,
testimonials: (b) => `
<div class="block-testimonials">
<div class="block-testimonials-header">
<h2>${b.content.title}</h2>
</div>
<div class="block-testimonials-grid">
${(b.content.items || []).map(item => `
<div class="block-testimonial-card">
<div class="block-testimonial-stars">
${'★'.repeat(item.rating || 5)}
</div>
<p class="block-testimonial-text">"${item.text}"</p>
<div class="block-testimonial-author">
<div class="block-testimonial-avatar">${(item.author || 'A')[0]}</div>
<span class="block-testimonial-name">${item.author}</span>
</div>
</div>
`).join('')}
</div>
</div>
`,
cta: (b) => `
<div class="block-cta">
<h2>${b.content.title}</h2>
<p>${b.content.text}</p>
<a href="${b.content.buttonLink || '#'}" class="block-cta-btn">
${b.content.buttonText}
<i class="ph ph-arrow-right"></i>
</a>
</div>
`,
prices: (b) => `
<div class="block-prices">
<div class="block-prices-header">
<h2>${b.content.title}</h2>
</div>
<div class="block-prices-list">
${(b.content.items || []).map(item => `
<div class="block-price-item">
<span class="block-price-name">${item.name}</span>
<span class="block-price-value">${item.price}</span>
</div>
`).join('')}
</div>
</div>
`,
faq: (b) => `
<div class="block-faq">
<div class="block-faq-header">
<h2>${b.content.title}</h2>
</div>
<div class="block-faq-list">
${(b.content.items || []).map(item => `
<div class="block-faq-item">
<div class="block-faq-question">
<span>${item.question}</span>
<i class="ph ph-caret-down"></i>
</div>
<div class="block-faq-answer">${item.answer}</div>
</div>
`).join('')}
</div>
</div>
`,
map: (b) => `
<div class="block-map">
<div class="block-map-placeholder">
<i class="ph ph-map-trifold"></i>
<p>${b.content.address}</p>
</div>
</div>
`,
social: (b) => {
const socialLinks = this.site.socialLinks || {};
const styleClass = b.content.style === 'outline' ? 'style-outline' : '';
const links = [];
if (socialLinks.instagram) links.push(`<a href="${socialLinks.instagram}" target="_blank" class="block-social-link ${styleClass}"><i class="ph ph-instagram-logo"></i></a>`);
if (socialLinks.facebook) links.push(`<a href="${socialLinks.facebook}" target="_blank" class="block-social-link ${styleClass}"><i class="ph ph-facebook-logo"></i></a>`);
if (socialLinks.tiktok) links.push(`<a href="${socialLinks.tiktok}" target="_blank" class="block-social-link ${styleClass}"><i class="ph ph-tiktok-logo"></i></a>`);
if (socialLinks.linkedin) links.push(`<a href="${socialLinks.linkedin}" target="_blank" class="block-social-link ${styleClass}"><i class="ph ph-linkedin-logo"></i></a>`);
if (links.length === 0) {
return `
<div class="block-social">
<h3>${b.content.title}</h3>
<p style="color: var(--color-text-muted);">Tilføj sociale medier i Design-fanen til venstre</p>
</div>
`;
}
return `
<div class="block-social">
<h3>${b.content.title}</h3>
<div class="block-social-links">
${links.join('')}
</div>
</div>
`;
}
};
return renderers[block.type] ? renderers[block.type](block) : `<div>Unknown block type: ${block.type}</div>`;
}
renderSettings() {
const content = document.getElementById('settingsContent');
const titleEl = document.getElementById('settingsTitle');
const block = this.getSelectedBlock();
const activeTab = document.querySelector('swp-settings-tab.active').dataset.tab;
if (!block) {
titleEl.textContent = 'Indstillinger';
content.innerHTML = `
<swp-settings-empty>
<i class="ph ph-cursor-click"></i>
<p>Vælg en blok for at redigere</p>
</swp-settings-empty>
`;
return;
}
titleEl.textContent = this.getBlockTitle(block.type);
if (activeTab === 'content') {
content.innerHTML = this.getContentSettings(block);
} else {
content.innerHTML = this.getStyleSettings(block);
}
// Add input handlers with debounce for IndexedDB
let saveTimeout = null;
// Content field handlers (block-specific)
content.querySelectorAll('[data-field]').forEach(input => {
input.addEventListener('input', () => {
const field = input.dataset.field;
const value = input.type === 'checkbox' ? input.checked : input.value;
// Update content immediately in memory
const page = this.getCurrentPage();
const currentBlock = page.blocks.find(b => b.id === this.selectedBlockId);
if (currentBlock) {
currentBlock.content[field] = value;
this.renderCanvas();
}
// Debounce the IndexedDB save
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
this.saveSite();
}, 300);
});
});
// Theme field handlers (color pickers, font select)
content.querySelectorAll('[data-theme-field]').forEach(input => {
input.addEventListener('input', () => {
const field = input.dataset.themeField;
const value = input.value;
// Update theme in memory
this.site.theme[field] = value;
// Update color value display
const colorValueSpan = input.parentElement.querySelector('.color-value');
if (colorValueSpan) {
colorValueSpan.textContent = value;
}
// Update CSS variables for live preview
this.applyTheme();
this.renderCanvas();
// Debounce save
clearTimeout(saveTimeout);
saveTimeout = setTimeout(() => {
this.saveSite();
}, 300);
});
});
// Button style picker
content.querySelectorAll('swp-button-style-option').forEach(option => {
option.addEventListener('click', () => {
content.querySelectorAll('swp-button-style-option').forEach(o => o.classList.remove('active'));
option.classList.add('active');
this.site.theme.buttonStyle = option.dataset.style;
this.applyTheme();
this.renderCanvas();
this.saveSite();
});
});
// Spacing picker
content.querySelectorAll('swp-spacing-option').forEach(option => {
option.addEventListener('click', () => {
content.querySelectorAll('swp-spacing-option').forEach(o => o.classList.remove('active'));
option.classList.add('active');
this.site.theme.spacing = option.dataset.spacing;
this.applyTheme();
this.renderCanvas();
this.saveSite();
});
});
}
applyTheme() {
const theme = this.site.theme;
const canvas = document.querySelector('swp-canvas-page');
if (canvas) {
const accent = theme.accentColor || '#f59e0b';
canvas.style.setProperty('--theme-primary', theme.primaryColor);
canvas.style.setProperty('--theme-secondary', theme.secondaryColor);
canvas.style.setProperty('--theme-accent', accent);
canvas.style.setProperty('--theme-text', theme.secondaryColor); // Secondary = text color
// Pre-computed accent backgrounds for hover states
canvas.style.setProperty('--theme-accent-bg-light', `${accent}1a`); // 10% opacity
canvas.style.setProperty('--theme-accent-bg-lighter', `${accent}14`); // 8% opacity
// Font family with proper quotes and fallback
const fontFamily = theme.fontFamily || 'Poppins';
const fontStack = `'${fontFamily}', sans-serif`;
canvas.style.setProperty('--theme-font', fontStack);
// Button border radius based on style
const buttonRadius = {
'rounded': '8px',
'pill': '50px',
'square': '0'
};
canvas.style.setProperty('--theme-button-radius', buttonRadius[theme.buttonStyle] || '8px');
// Spacing multiplier
const spacingMultiplier = {
'compact': '0.75',
'normal': '1',
'spacious': '1.5'
};
canvas.style.setProperty('--theme-spacing', spacingMultiplier[theme.spacing] || '1');
}
}
renderLibraryDesign() {
const currentScheme = this.site.theme.colorScheme || 'salon-classic';
const currentFont = this.site.theme.fontFamily || 'Poppins';
// Render color schemes
const schemesContainer = document.getElementById('libraryColorSchemes');
schemesContainer.innerHTML = colorSchemes.map(scheme => `
<swp-color-scheme data-scheme="${scheme.id}" class="${scheme.id === currentScheme ? 'active' : ''}">
<swp-color-scheme-colors>
<span style="background: ${scheme.colors.primary}"></span>
<span style="background: ${scheme.colors.secondary}"></span>
<span style="background: ${scheme.colors.accent}"></span>
<span style="background: ${scheme.colors.background}"></span>
</swp-color-scheme-colors>
<swp-color-scheme-name>${scheme.name}</swp-color-scheme-name>
</swp-color-scheme>
`).join('');
// Update font cards active state
document.querySelectorAll('swp-font-card').forEach(card => {
card.classList.toggle('active', card.dataset.font === currentFont);
});
// Update social links inputs
const socialLinks = this.site.socialLinks || {};
document.querySelectorAll('#socialLinksSettings input[data-social]').forEach(input => {
input.value = socialLinks[input.dataset.social] || '';
});
}
getBlockTitle(type) {
const titles = {
hero: 'Hero Sektion',
columns: 'Kolonner',
about: 'Tekst',
spacer: 'Spacer',
divider: 'Divider',
services: 'Ydelser',
team: 'Team',
gallery: 'Galleri',
testimonials: 'Anmeldelser',
cta: 'CTA Banner',
prices: 'Prisliste',
faq: 'FAQ',
contact: 'Kontakt',
hours: 'Åbningstider',
booking: 'Booking',
map: 'Kort',
social: 'Sociale Medier'
};
return titles[type] || type;
}
getContentSettings(block) {
const settingsMap = {
hero: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Overskrift</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="headline" value="${block.content.headline || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Underoverskrift</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="subheadline" value="${block.content.subheadline || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Baggrundsbillede URL</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="backgroundImage" value="${block.content.backgroundImage || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Knap tekst</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="ctaText" value="${block.content.ctaText || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Knap link</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="ctaLink" value="${block.content.ctaLink || ''}">
</swp-edit-row>
</swp-edit-section>
`,
about: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Tekst</swp-edit-label>
<textarea class="swp-edit-textarea" data-field="text">${block.content.text || ''}</textarea>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Billede URL</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="imageUrl" value="${block.content.imageUrl || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Billede position</swp-edit-label>
<swp-edit-select>
<select data-field="imagePosition">
<option value="right" ${block.content.imagePosition === 'right' ? 'selected' : ''}>Højre</option>
<option value="left" ${block.content.imagePosition === 'left' ? 'selected' : ''}>Venstre</option>
</select>
</swp-edit-select>
</swp-edit-row>
</swp-edit-section>
`,
spacer: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Størrelse</swp-edit-label>
<swp-edit-select>
<select data-field="size">
<option value="small" ${block.content.size === 'small' ? 'selected' : ''}>Lille (30px)</option>
<option value="medium" ${block.content.size === 'medium' ? 'selected' : ''}>Medium (60px)</option>
<option value="large" ${block.content.size === 'large' ? 'selected' : ''}>Stor (100px)</option>
</select>
</swp-edit-select>
</swp-edit-row>
</swp-edit-section>
`,
services: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Undertitel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="subtitle" value="${block.content.subtitle || ''}">
</swp-edit-row>
</swp-edit-section>
<swp-settings-hint>
<i class="ph ph-info"></i>
Ydelser hentes automatisk fra systemet.
</swp-settings-hint>
`,
team: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
</swp-edit-section>
<swp-settings-hint>
<i class="ph ph-info"></i>
Medarbejdere hentes automatisk fra systemet.
</swp-settings-hint>
`,
contact: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Adresse</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="address" value="${block.content.address || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Telefon</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="phone" value="${block.content.phone || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Email</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="email" value="${block.content.email || ''}">
</swp-edit-row>
</swp-edit-section>
`,
hours: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
</swp-edit-section>
<swp-settings-hint>
<i class="ph ph-info"></i>
Åbningstider kan redigeres i Indstillinger.
</swp-settings-hint>
`,
booking: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Undertitel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="subtitle" value="${block.content.subtitle || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Knap tekst</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="buttonText" value="${block.content.buttonText || ''}">
</swp-edit-row>
</swp-edit-section>
`,
columns: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Layout</swp-edit-label>
<swp-edit-select>
<select data-field="layout">
<option value="2" ${block.content.layout === '2' ? 'selected' : ''}>2 kolonner (50/50)</option>
<option value="3" ${block.content.layout === '3' ? 'selected' : ''}>3 kolonner</option>
<option value="1-2" ${block.content.layout === '1-2' ? 'selected' : ''}>2 kolonner (33/66)</option>
<option value="2-1" ${block.content.layout === '2-1' ? 'selected' : ''}>2 kolonner (66/33)</option>
</select>
</swp-edit-select>
</swp-edit-row>
</swp-edit-section>
<swp-section-title>Kolonne 1</swp-section-title>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="column1Title" value="${block.content.column1Title || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Tekst</swp-edit-label>
<textarea class="swp-edit-textarea" data-field="column1Text">${block.content.column1Text || ''}</textarea>
</swp-edit-row>
</swp-edit-section>
<swp-section-title>Kolonne 2</swp-section-title>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="column2Title" value="${block.content.column2Title || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Tekst</swp-edit-label>
<textarea class="swp-edit-textarea" data-field="column2Text">${block.content.column2Text || ''}</textarea>
</swp-edit-row>
</swp-edit-section>
${block.content.layout === '3' ? `
<swp-section-title>Kolonne 3</swp-section-title>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="column3Title" value="${block.content.column3Title || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Tekst</swp-edit-label>
<textarea class="swp-edit-textarea" data-field="column3Text">${block.content.column3Text || ''}</textarea>
</swp-edit-row>
</swp-edit-section>
` : ''}
`,
divider: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Stil</swp-edit-label>
<swp-edit-select>
<select data-field="style">
<option value="normal" ${block.content.style === 'normal' ? 'selected' : ''}>Normal</option>
<option value="thick" ${block.content.style === 'thick' ? 'selected' : ''}>Tyk</option>
<option value="dashed" ${block.content.style === 'dashed' ? 'selected' : ''}>Stiplet</option>
</select>
</swp-edit-select>
</swp-edit-row>
</swp-edit-section>
`,
gallery: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
</swp-edit-section>
<swp-settings-hint>
<i class="ph ph-info"></i>
Billeder kan uploades i Media biblioteket.
</swp-settings-hint>
`,
testimonials: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
</swp-edit-section>
<swp-settings-hint>
<i class="ph ph-info"></i>
Anmeldelser kan administreres i Indstillinger.
</swp-settings-hint>
`,
cta: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Tekst</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="text" value="${block.content.text || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Knap tekst</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="buttonText" value="${block.content.buttonText || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Knap link</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="buttonLink" value="${block.content.buttonLink || ''}">
</swp-edit-row>
</swp-edit-section>
`,
prices: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
</swp-edit-section>
<swp-settings-hint>
<i class="ph ph-info"></i>
Priser hentes fra Ydelser-indstillinger.
</swp-settings-hint>
`,
faq: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Titel</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
</swp-edit-section>
<swp-settings-hint>
<i class="ph ph-info"></i>
FAQ spørgsmål kan redigeres i Indstillinger.
</swp-settings-hint>
`,
map: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Adresse</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="address" value="${block.content.address || ''}">
</swp-edit-row>
</swp-edit-section>
<swp-settings-hint>
<i class="ph ph-info"></i>
Google Maps integration kræver API-nøgle.
</swp-settings-hint>
`,
social: `
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Overskrift</swp-edit-label>
<input type="text" class="swp-edit-value" data-field="title" value="${block.content.title || ''}">
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Ikon-stil</swp-edit-label>
<swp-edit-select>
<select data-field="style">
<option value="filled" ${block.content.style === 'filled' ? 'selected' : ''}>Udfyldt</option>
<option value="outline" ${block.content.style === 'outline' ? 'selected' : ''}>Kontur</option>
</select>
</swp-edit-select>
</swp-edit-row>
</swp-edit-section>
<swp-settings-hint>
<i class="ph ph-info"></i>
Tilføj sociale medie-links i Design-fanen til venstre.
</swp-settings-hint>
`
};
return settingsMap[block.type] || '<swp-settings-hint><i class="ph ph-info"></i>Ingen indstillinger for denne blok</swp-settings-hint>';
}
getStyleSettings(block) {
const theme = this.site.theme;
return `
<swp-settings-hint style="margin-bottom: 16px;">
<i class="ph ph-palette"></i>
Vælg farveskema og typografi i Design-fanen til venstre
</swp-settings-hint>
<swp-section-title>Tilpas farver</swp-section-title>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Primær farve</swp-edit-label>
<swp-color-picker-row>
<input type="color" value="${theme.primaryColor}" data-theme-field="primaryColor">
<span class="color-value">${theme.primaryColor}</span>
</swp-color-picker-row>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Sekundær farve</swp-edit-label>
<swp-color-picker-row>
<input type="color" value="${theme.secondaryColor}" data-theme-field="secondaryColor">
<span class="color-value">${theme.secondaryColor}</span>
</swp-color-picker-row>
</swp-edit-row>
<swp-edit-row>
<swp-edit-label>Accent farve</swp-edit-label>
<swp-color-picker-row>
<input type="color" value="${theme.accentColor || '#f59e0b'}" data-theme-field="accentColor">
<span class="color-value">${theme.accentColor || '#f59e0b'}</span>
</swp-color-picker-row>
</swp-edit-row>
</swp-edit-section>
<swp-section-title>Knapper</swp-section-title>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Knap-stil</swp-edit-label>
<swp-button-style-picker>
<swp-button-style-option data-style="rounded" ${theme.buttonStyle === 'rounded' || !theme.buttonStyle ? 'class="active"' : ''}>
<span></span>
</swp-button-style-option>
<swp-button-style-option data-style="pill" ${theme.buttonStyle === 'pill' ? 'class="active"' : ''}>
<span></span>
</swp-button-style-option>
<swp-button-style-option data-style="square" ${theme.buttonStyle === 'square' ? 'class="active"' : ''}>
<span></span>
</swp-button-style-option>
</swp-button-style-picker>
</swp-edit-row>
</swp-edit-section>
<swp-section-title>Layout</swp-section-title>
<swp-edit-section>
<swp-edit-row>
<swp-edit-label>Afstand</swp-edit-label>
<swp-spacing-picker>
<swp-spacing-option data-spacing="compact" ${theme.spacing === 'compact' ? 'class="active"' : ''}>
<i class="ph ph-arrows-in-line-vertical"></i>
<span>Kompakt</span>
</swp-spacing-option>
<swp-spacing-option data-spacing="normal" ${theme.spacing === 'normal' || !theme.spacing ? 'class="active"' : ''}>
<i class="ph ph-arrows-out-line-vertical"></i>
<span>Normal</span>
</swp-spacing-option>
<swp-spacing-option data-spacing="spacious" ${theme.spacing === 'spacious' ? 'class="active"' : ''}>
<i class="ph ph-arrows-vertical"></i>
<span>Luftig</span>
</swp-spacing-option>
</swp-spacing-picker>
</swp-edit-row>
</swp-edit-section>
`;
}
}
// Initialize
const builder = new WebsiteBuilderState();
builder.initialize().catch(err => {
console.error('Failed to initialize Website Builder:', err);
});
</script>
</body>
</html>