editor.html
html
<!DOCTYPE html>
<html lang="en" data-theme="dark">
<head>
  {{template "head"}}
  <title>SkyDeck Editor</title>
  <!-- CodeMirror for code blocks -->
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.css">
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/theme/dracula.min.css">
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/codemirror.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/javascript/javascript.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/python/python.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/go/go.min.js"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/codemirror/5.65.16/mode/markdown/markdown.min.js"></script>
  <!-- Sortable for drag/drop -->
  <script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.2/Sortable.min.js"></script>
  <!-- Marked for markdown preview -->
  <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
  <!-- Editor styles -->
  <link rel="stylesheet" href="/public/styles/editor.css" />
  <link rel="stylesheet" href="/public/styles/formatting-toolbar.css" />
</head>
<body class="min-h-screen bg-base-200 flex flex-col">
  <!-- Formatting Toolbar (positioned by JS) -->
  <div id="formatting-toolbar"></div>
  <!-- Editor Navbar -->
  <nav class="navbar bg-base-100/80 backdrop-blur-md border-b border-white/5 px-4 flex-none sticky top-0 z-50">
    <div class="navbar-start gap-2">
      <!-- Mobile: Slides toggle -->
      <button class="btn btn-ghost btn-sm md:hidden" onclick="toggleSidebar('slides')">
        <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/>
          <rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>
        </svg>
      </button>
      <a href="/" class="btn btn-ghost btn-sm gap-1">
        {{template "icon-logo"}}
        <span class="hidden sm:inline">SkyDeck</span>
      </a>
      <span class="text-base-content/30 hidden sm:inline">/</span>
      <input type="text" id="deck-title" class="input input-ghost text-lg font-semibold flex-1 min-w-0 sm:flex-none sm:w-56"
        placeholder="Untitled Deck" value="{{with decks.CurrentDeck}}{{.Title}}{{end}}" />
      <span id="save-status" class="text-xs text-base-content/50 hidden sm:inline">Saved</span>
    </div>
    <div class="navbar-end gap-2">
      {{with decks.CurrentDeck}}
      <div class="hidden lg:flex">
        {{template "btn-publish" .}}
      </div>
      {{end}}
      <!-- Mobile: Settings toggle -->
      <button class="btn btn-ghost btn-sm lg:hidden" onclick="toggleSidebar('settings')">
        <svg class="w-5 h-5" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
          <path stroke-linecap="round" stroke-linejoin="round" d="M10.5 6h9.75M10.5 6a1.5 1.5 0 1 1-3 0m3 0a1.5 1.5 0 1 0-3 0M3.75 6H7.5m3 12h9.75m-9.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-3.75 0H7.5m9-6h3.75m-3.75 0a1.5 1.5 0 0 1-3 0m3 0a1.5 1.5 0 0 0-3 0m-9.75 0h9.75"/>
        </svg>
      </button>
      {{template "user-menu"}}
    </div>
  </nav>
  <!-- Main editor -->
  <div class="flex-1 flex overflow-hidden">
    <!-- Slide sidebar -->
    <aside id="slides-sidebar" class="hidden md:flex w-64 bg-base-200 border-r border-white/5 flex-col flex-shrink-0 absolute md:relative z-40 h-full shadow-xl md:shadow-none">
      <div class="px-4 py-3 border-b border-white/5 flex items-center justify-between">
        <button id="btn-add-slide" class="btn btn-ghost btn-sm flex-1">
          <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path stroke-linecap="round" stroke-linejoin="round" d="M12 4v16m8-8H4" />
          </svg>
          Add Slide
        </button>
        <button class="btn btn-ghost btn-xs md:hidden" onclick="toggleSidebar('slides')">
          <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
        </button>
      </div>
      <div id="slides-list" class="flex-1 overflow-y-auto px-4 py-3 space-y-4">
        <!-- Slide thumbnails rendered by JS -->
      </div>
    </aside>
    <!-- Slide canvas -->
    <main class="flex-1 flex flex-col items-center justify-center p-4 md:p-8 overflow-auto bg-base-300">
      <!-- Canvas wrapper for centering scaled content -->
      <div id="canvas-wrapper" class="flex items-center justify-center">
        <div id="slide-canvas" class="relative bg-base-100 rounded-lg shadow-2xl overflow-hidden" data-theme="{{with decks.CurrentDeckContent}}{{.Theme}}{{else}}dark{{end}}">
          <div id="blocks-container" class="absolute inset-0 p-[5%] overflow-y-auto flex flex-col justify-center">
            <!-- Blocks rendered by JS -->
          </div>
        </div>
      </div>
    </main>
    <!-- Right panel (settings) -->
    <aside id="settings-panel" class="hidden lg:block w-64 bg-base-200 border-l border-white/5 p-4 flex-shrink-0 absolute lg:relative right-0 z-40 h-full shadow-xl lg:shadow-none overflow-y-auto">
      <div class="flex items-center justify-between mb-4">
        <h3 class="font-semibold">Deck Settings</h3>
        <button class="btn btn-ghost btn-xs lg:hidden" onclick="toggleSidebar('settings')">
          <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
            <path stroke-linecap="round" stroke-linejoin="round" d="M6 18L18 6M6 6l12 12" />
          </svg>
        </button>
      </div>
      <!-- Publish/Present controls (shown in sidebar on mobile) -->
      {{with decks.CurrentDeck}}
      <div class="lg:hidden mb-4 pb-4 border-b border-white/10">
        {{template "btn-publish" .}}
      </div>
      {{end}}
      <!-- Cover Image -->
      <div class="form-control mb-4">
        <label class="label">
          <span class="label-text">Cover Image</span>
        </label>
        {{with decks.CurrentDeck}}
        {{template "cover-preview" .}}
        <div class="flex gap-2">
          <form hx-post="/decks/{{.ID}}/cover" hx-target="#cover-preview" hx-swap="outerHTML" hx-encoding="multipart/form-data" class="flex-1">
            <label class="btn btn-ghost btn-sm w-full">
              <input type="file" name="image" class="hidden" accept="image/*" onchange="this.form.requestSubmit()" />
              Upload
            </label>
          </form>
          <button hx-delete="/decks/{{.ID}}/cover" hx-target="#cover-preview" hx-swap="outerHTML" class="btn btn-ghost btn-sm">Remove</button>
        </div>
        {{end}}
      </div>
      <!-- Theme selector -->
      {{$theme := "dark"}}{{with decks.CurrentDeckContent}}{{$theme = .Theme}}{{end}}
      <div class="form-control mb-4">
        <label class="label">
          <span class="label-text">Theme</span>
        </label>
        {{with decks.CurrentDeck}}
        <select id="theme-select" name="theme" class="select select-bordered select-sm w-full"
          hx-put="/decks/{{.ID}}/theme"
          hx-trigger="change"
          hx-swap="none"
          hx-on::after-request="applyEditorTheme(this.value)">
          <option value="dark" {{if eq $theme "dark"}}selected{{end}}>Dark</option>
          <option value="light" {{if eq $theme "light"}}selected{{end}}>Light</option>
          <option value="cyberpunk" {{if eq $theme "cyberpunk"}}selected{{end}}>Cyberpunk</option>
          <option value="synthwave" {{if eq $theme "synthwave"}}selected{{end}}>Synthwave</option>
          <option value="forest" {{if eq $theme "forest"}}selected{{end}}>Forest</option>
          <option value="aqua" {{if eq $theme "aqua"}}selected{{end}}>Aqua</option>
          <option value="dracula" {{if eq $theme "dracula"}}selected{{end}}>Dracula</option>
          <option value="night" {{if eq $theme "night"}}selected{{end}}>Night</option>
          <option value="coffee" {{if eq $theme "coffee"}}selected{{end}}>Coffee</option>
          <option value="luxury" {{if eq $theme "luxury"}}selected{{end}}>Luxury</option>
        </select>
        {{end}}
      </div>
      <!-- Slide Background -->
      <div class="form-control mb-4">
        <label class="label">
          <span class="label-text">Slide Background</span>
        </label>
        <div id="slide-background-controls" class="space-y-2">
          <select id="slide-bg-type" class="select select-bordered select-sm w-full" onchange="updateSlideBackgroundType(this.value)">
            <option value="">None (Theme Default)</option>
            <option value="color">Solid Color</option>
            <option value="image">Image</option>
          </select>
          <div id="slide-bg-color-controls" class="hidden space-y-2">
            <div class="flex gap-2">
              <input type="color" id="slide-bg-color" class="w-10 h-10 cursor-pointer rounded"
                     value="#000000"
                     onchange="updateSlideBackground({ color: this.value })" />
              <input type="text" id="slide-bg-color-text" class="input input-bordered input-sm flex-1"
                     placeholder="#000000"
                     onchange="updateSlideBackground({ color: this.value })" />
            </div>
            <div class="form-control">
              <label class="label py-1">
                <span class="label-text text-xs">Opacity</span>
              </label>
              <input type="range" id="slide-bg-opacity" class="range range-xs range-primary"
                     min="0" max="1" step="0.1" value="1"
                     onchange="updateSlideBackground({ opacity: this.value })" />
            </div>
          </div>
          <div id="slide-bg-image-controls" class="hidden space-y-2">
            <input type="text" id="slide-bg-image-url" class="input input-bordered input-sm w-full"
                   placeholder="Image URL"
                   onchange="updateSlideBackground({ imageURL: this.value })" />
            <select id="slide-bg-position" class="select select-bordered select-sm w-full"
                    onchange="updateSlideBackground({ position: this.value })">
              <option value="cover">Cover</option>
              <option value="contain">Contain</option>
              <option value="center">Center</option>
            </select>
          </div>
        </div>
      </div>
      <!-- Block Properties (shown when block is selected) -->
      <div id="property-panel-content" class="border-t border-white/10 pt-4 mt-4">
        <div class="text-center text-base-content/50 py-8">
          <p class="text-sm">Select a block to edit its properties</p>
        </div>
      </div>
      <!-- Add Block buttons -->
      <div class="border-t border-white/10 pt-4 mt-4">
        <h4 class="text-sm font-medium mb-3 text-base-content/70">Add Block</h4>
        <div class="grid grid-cols-2 gap-2">
          <button onclick="addBlock('heading')" class="btn btn-ghost btn-sm justify-start">
            <span class="text-lg font-bold">H</span> Heading
          </button>
          <button onclick="addBlock('text')" class="btn btn-ghost btn-sm justify-start">
            <span class="text-sm">Aa</span> Text
          </button>
          <button onclick="addBlock('code')" class="btn btn-ghost btn-sm justify-start">
            <span class="font-mono text-xs">&lt;/&gt;</span> Code
          </button>
          <button onclick="addBlock('image')" class="btn btn-ghost btn-sm justify-start">
            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
              <rect x="3" y="3" width="18" height="18" rx="2" ry="2"/>
              <circle cx="8.5" cy="8.5" r="1.5"/>
              <path d="m21 15-5-5L5 21"/>
            </svg>
            Image
          </button>
          <button onclick="addBlock('list')" class="btn btn-ghost btn-sm justify-start">
            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
              <line x1="8" y1="6" x2="21" y2="6"/>
              <line x1="8" y1="12" x2="21" y2="12"/>
              <line x1="8" y1="18" x2="21" y2="18"/>
              <line x1="3" y1="6" x2="3.01" y2="6"/>
              <line x1="3" y1="12" x2="3.01" y2="12"/>
              <line x1="3" y1="18" x2="3.01" y2="18"/>
            </svg>
            List
          </button>
          <button onclick="addBlock('quote')" class="btn btn-ghost btn-sm justify-start">
            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="currentColor">
              <path d="M6 17h3l2-4V7H5v6h3zm8 0h3l2-4V7h-6v6h3z"/>
            </svg>
            Quote
          </button>
          <button onclick="addBlock('divider')" class="btn btn-ghost btn-sm justify-start col-span-2">
            <svg class="w-4 h-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
              <line x1="5" y1="12" x2="19" y2="12"/>
            </svg>
            Divider
          </button>
        </div>
      </div>
    </aside>
  </div>
  <!-- Deck list modal -->
  <dialog id="decks-modal" class="modal">
    <div class="modal-box">
      <h3 class="font-bold text-lg mb-4">Your Decks</h3>
      <div id="decks-list" class="space-y-2">
        <!-- Decks loaded by JS -->
      </div>
      <div class="modal-action">
        <button id="btn-new-deck" class="btn btn-primary">New Deck</button>
        <form method="dialog">
          <button class="btn">Close</button>
        </form>
      </div>
    </div>
    <form method="dialog" class="modal-backdrop">
      <button>close</button>
    </form>
  </dialog>
  <!-- Initial deck data -->
  <script>
    window.DECK_ID = "{{with decks.CurrentDeck}}{{.ID}}{{end}}";
    // Content as string to be parsed - same pattern as preview.html
    window.DECK_DATA = "{{with decks.CurrentDeck}}{{.Content}}{{end}}";
    window.IS_PUBLISHED = {{with decks.CurrentDeck}}{{.IsPublished}}{{else}}false{{end}};
  </script>
  <script src="/public/scripts/utils/style-generator.js"></script>
  <script src="/public/scripts/components/formatting-toolbar.js"></script>
  <script src="/public/scripts/components/property-panel.js"></script>
  <script src="/public/scripts/editor.js"></script>
</body>
</html>
72f0edf

Add rich text formatting and block styling to editor

Connor McCutcheon
@connor
0 stars

Pitch decks to help promote Skyscape Apps

Sign in to comment Sign In