Week 7 of 16

Build: Polish the UI

Apply your DVP design skills — color system, typography, card styling, hover effects, copy button

Day 34 60 minutes Build

Day 34 of 80

Today Is Your Territory

HTML and CSS Are What You Already Know

You've been designing web interfaces for years. Today the Python work is done — the routes are built, the data flows correctly, Claude generates prompts. Now you make the Prompt Vault look as polished as any DVP tool you've shipped. The inline styles from Day 28 were functional. Today you replace them with a proper design system.

A polished tool gets used. An ugly tool gets abandoned. This isn't cosmetic work — it's product work.

Design Decisions to Make

Before touching code, make these four decisions. Write them down. Good design starts with constraints.

Decision Recommendation Your Choice
Background color #0a0a0a (near-black)
Card surface color #111 or #141414
Accent / primary color Your DVP brand blue, or #2563eb
Body font Inter (already loaded via Google Fonts)
Code / monospace font JetBrains Mono (already loaded)

Platform Tag Colors

Each AI video platform gets its own color. Consistent, recognizable, and instantly scannable when browsing the card grid:

CSS — platform tag colors CSS
/* Platform tags */
.platform-tag {
  display: inline-block;
  font-size: 0.7rem;
  font-weight: 700;
  letter-spacing: 0.05em;
  text-transform: uppercase;
  padding: 0.2rem 0.65rem;
  border-radius: 20px;
}

.platform-kling  { background: #1d4ed8; color: #fff; }   /* Blue */
.platform-runway { background: #7c3aed; color: #fff; }   /* Purple */
.platform-veo    { background: #059669; color: #fff; }   /* Green */
.platform-sora   { background: #b45309; color: #fff; }   /* Amber — add if needed */
.platform-pika   { background: #be185d; color: #fff; }   /* Pink — add if needed */
Keep platform colors semantically distinct — not just aesthetically pleasing. Kling blue, Runway purple, Veo green should be immediately recognizable after a week of using the app. If you add more platforms later, pick colors that don't conflict with existing ones.

Prompt Card Styling

CSS — prompt cards CSS
/* Card grid */
.prompt-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(340px, 1fr));
  gap: 1rem;
  margin: 1.5rem 0;
}

.prompt-card {
  background: #111;
  border: 1px solid #1e1e1e;
  border-radius: 10px;
  padding: 1.25rem;
  transition: border-color 0.15s, transform 0.15s;
  cursor: default;
}

.prompt-card:hover {
  border-color: #2a2a2a;
  transform: translateY(-1px);
}

.card-header {
  display: flex;
  justify-content: space-between;
  align-items: flex-start;
  margin-bottom: 0.75rem;
}

.shot-label {
  font-size: 0.8rem;
  font-weight: 600;
  color: #666;
  text-transform: uppercase;
  letter-spacing: 0.04em;
  margin-bottom: 0.5rem;
}

.prompt-text {
  font-size: 0.9rem;
  color: #bbb;
  line-height: 1.6;
  margin-bottom: 0.75rem;
}

.card-actions {
  display: flex;
  gap: 0.5rem;
  justify-content: flex-end;
  padding-top: 0.75rem;
  border-top: 1px solid #1e1e1e;
}

.btn-copy {
  background: #1a1a1a;
  border: 1px solid #2a2a2a;
  color: #888;
  padding: 0.3rem 0.8rem;
  border-radius: 5px;
  font-size: 0.8rem;
  cursor: pointer;
  transition: all 0.15s;
}

.btn-copy:hover { color: #e5e5e5; border-color: #444; }

.btn-delete {
  background: transparent;
  border: 1px solid transparent;
  color: #444;
  padding: 0.3rem 0.8rem;
  border-radius: 5px;
  font-size: 0.8rem;
  text-decoration: none;
  transition: all 0.15s;
}

.btn-delete:hover { color: #ef4444; border-color: #3f1f1f; background: #1a0a0a; }
Subtle hovertranslateY(-1px) lifts the card 1 pixel on hover. Combined with the border color change, it gives tactile feedback without being distracting. Keep transforms small on data-dense UIs.

Delete button is intentionally subtle — Muted color (#444) until hover, where it turns red. The visual weight matches the action's danger level: it's there when you need it, invisible when you don't.

Button Hierarchy

CSS — button system CSS
/* Primary action */
.btn-primary {
  background: #2563eb;
  color: white;
  border: none;
  padding: 0.65rem 1.5rem;
  border-radius: 7px;
  font-weight: 600;
  font-size: 0.9rem;
  cursor: pointer;
  transition: background 0.15s;
}
.btn-primary:hover { background: #1d4ed8; }

/* Generate button — slightly different style */
.btn-generate {
  background: #1e3a8a;
  color: #93c5fd;
  border: 1px solid #1d4ed8;
  padding: 0.65rem 1.25rem;
  border-radius: 7px;
  font-weight: 600;
  font-size: 0.9rem;
  cursor: pointer;
  white-space: nowrap;
  transition: all 0.15s;
}
.btn-generate:hover { background: #1d4ed8; color: white; }

/* Save generated prompt */
.btn-save {
  background: #052e16;
  color: #4ade80;
  border: 1px solid #166534;
  padding: 0.6rem 1.25rem;
  border-radius: 7px;
  font-weight: 600;
  font-size: 0.9rem;
  cursor: pointer;
  transition: all 0.15s;
}
.btn-save:hover { background: #166534; color: white; }
Three distinct button styles for three distinct action types: blue for primary/neutral actions (Add Prompt), dark-blue for AI actions (Generate), green for save/commit actions. Color coding action types makes the UI self-documenting — the button tells you what kind of action it is before you read the label.

Copy to Clipboard

Add this JavaScript to the bottom of index.html, just before </body>, and the corresponding button in each card's card-actions section:

JavaScript — copy to clipboard JavaScript
<script>
function copyPrompt(btn, text) {
  navigator.clipboard.writeText(text)
    .then(() => {
      const original = btn.textContent;
      btn.textContent = "Copied!";
      btn.style.color = "#4ade80";
      btn.style.borderColor = "#166534";
      setTimeout(() => {
        btn.textContent = original;
        btn.style.color = "";
        btn.style.borderColor = "";
      }, 1500);
    })
    .catch(() => {
      // Fallback: select the text programmatically
      const el = document.createElement("textarea");
      el.value = text;
      document.body.appendChild(el);
      el.select();
      document.execCommand("copy");
      document.body.removeChild(el);
    });
}
</script>
navigator.clipboard.writeText() — The modern Clipboard API. Returns a Promise. Only works on HTTPS or localhost — which is exactly where you're running Flask in dev.

Visual feedback — Change the button text to "Copied!" and its color to green for 1.5 seconds, then restore the original state. Storing original before changing means this works correctly even if the user clicks the button twice quickly.

.catch() fallback — Some browsers (older, non-HTTPS) don't support the Clipboard API. The fallback creates a hidden textarea, selects its text, and uses the old execCommand("copy") approach. It's less elegant but universally compatible.

In each prompt card in index.html, add the copy button using a data- attribute approach (cleaner than inline string escaping):

templates/index.html — copy button in card HTML + Jinja2
<div class="card-actions">
  <button class="btn-copy"
          data-prompt="{{ p.prompt | e }}"
          onclick="copyPrompt(this, this.dataset.prompt)">
    Copy
  </button>
  <a href="/delete/{{ loop.index0 }}"
     class="btn-delete"
     onclick="return confirm('Delete this prompt?')">
    Delete
  </a>
</div>
data-prompt="{{ p.prompt | e }}" — Stores the prompt text in a data- attribute on the button element. The | e filter (short for escape) encodes HTML special characters so quotes in the prompt don't break the attribute. Then JavaScript reads it with this.dataset.prompt — clean, no inline escaping gymnastics.

This is the correct pattern for passing Python strings into JavaScript: store in a data- attribute with HTML escaping, read with dataset in JavaScript. Always safer than embedding strings directly in onclick handlers.

Stretch Goals

If You Have Time

Toast notifications — Instead of the button changing text, pop a small "Copied!" toast in the bottom-right corner. This involves creating a <div> dynamically in JavaScript, animating it in, and removing it after 2 seconds. A nice exercise in DOM manipulation.

Keyboard shortcut for search — Add a keydown listener: pressing / focuses the search input. Standard convention on many search-heavy tools (GitHub uses it). About 5 lines of JavaScript.

Responsive mobile layout — The grid already uses auto-fill with minmax(340px, 1fr), which collapses to a single column on narrow screens. Add a media query to adjust font sizes and padding for small screens. Test by narrowing your browser window.

Empty state per filter — When filtering to a platform with no prompts, show a friendly message: "No Runway prompts yet. Generate one above." Uses {% if not prompts %} and the active_filter variable.

End of Day Checklist

Tomorrow — Week 7 Review

Day 35 is the Week 7 review. You'll consolidate everything you built this week, verify all features work together cleanly, and get a preview of Week 8: error handling, edge cases, and deploying the Prompt Vault somewhere people can actually use it.