Skip to content

Instantly share code, notes, and snippets.

@tuhuynh27
Created March 25, 2026 12:10
Show Gist options
  • Select an option

  • Save tuhuynh27/5469da743133d5fae72d3ca3f11ae86a to your computer and use it in GitHub Desktop.

Select an option

Save tuhuynh27/5469da743133d5fae72d3ca3f11ae86a to your computer and use it in GitHub Desktop.
TCPW EI

How the HTML integrates with the app

Rendering context

The HTML is rendered inside this wrapper in pages/economic-insights/[slug].vue:

<div class="insight-html-content" :data-locale="locale" v-html="insight.body_html"></div>
  • The parent page handles: back navigation, page header (title/subtitle/eyebrow), access gate (MAX tier), SEO meta tags, and activity tracking.
  • Your HTML is ONLY the page body content — no page header, no access gate, no <html>/<head>/<body> tags.

Language switching (vi/en)

The wrapper div has data-locale="vi" or data-locale="en" based on the user's current locale. The app injects this CSS:

.insight-html-content[data-locale="vi"] [data-lang="en"] { display: none !important; }
.insight-html-content[data-locale="en"] [data-lang="vi"] { display: none !important; }

Convention: Wrap all translatable text in paired spans:

<span data-lang="vi">Vietnamese text here</span><span data-lang="en">English text here</span>

Content that is identical in both languages (numbers, dollar amounts, percentages, proper names, dates, ticker symbols) should NOT be wrapped — leave as plain text.

Theme switching (light/dark)

The app sets data-theme="light" or data-theme="dark" on the <html> element. Your HTML inherits this automatically. Use the app's CSS variables for all colors:

Available CSS Variables

Variable Light Dark Usage
--bg-color #ffffff #1a1a1a Page background
--text-color #1a1a1a #ffffff Primary text
--text-secondary #666 #b3b3b3 Secondary text, descriptions
--text-tertiary #999 #888 Tertiary text, captions, timestamps
--border-color #e1e1e1 #333 Borders, dividers
--border-light #e5e5e5 #444 Lighter borders
--border-lighter #f5f5f5 #2a2a2a Subtlest borders
--card-bg #ffffff #222 Card backgrounds
--surface-1 #ffffff #2a2a2a Elevated surface
--surface-2 #f8f9fa #333 Section backgrounds, callouts
--surface-3 #f0f0f0 #3a3a3a Deeper surface
--hover-bg #f0f1f2 #3a3a3a Hover states
--input-bg #f8f9fa #2a2a2a Input fields
--accent-color #10b981 #10b981 Accent/brand color (emerald)
--positive-color #2d7d32 #4caf50 Positive/green values
--positive-bg #e8f5e8 #1a3d1a Positive background
--negative-color #c33 #f44336 Negative/red values
--negative-bg #fee #3d1a1a Negative background
--primary-color #374151 #d1d5db Primary buttons
--tab-bg #f8f9fa #2a2a2a Tab backgrounds
--tab-hover-bg #f0f1f2 #3a3a3a Tab hover
--warning-bg #fff3cd #3d3a1a Warning backgrounds
--warning-color #856404 #ffc107 Warning text

Rules:

  • ALWAYS use var(--xxx) for text, backgrounds, and borders — never hardcode #fff or #000.
  • For brand/semantic colors (red for critical, blue for coalition, etc.) use direct hex values — these don't change with theme.
  • When brand colors need a translucent background, use rgba() with low opacity (0.08–0.15). If dark mode needs a different opacity, add an explicit override:
    .my-badge { background: rgba(220, 38, 38, 0.08); }
    [data-theme="dark"] .my-badge { background: rgba(220, 38, 38, 0.14); }

Script execution

The app automatically executes <script> tags found in the HTML content after rendering. You can include vanilla JS for interactivity (tab switching, filters, accordions, sliders, etc.). Scripts run inside the page DOM — they have access to document.querySelector etc.

Important: Use an IIFE to avoid polluting the global scope. Expose interactive functions on window with a unique prefix:

<script>
(function() {
  function switchTab(key) { /* ... */ }
  window.__myPrefixSwitchTab = switchTab;
  switchTab('default');
})();
</script>

HTML file structure

<style>
/* All CSS for this page — no scoping needed */
/* Use CSS variables from the table above for theme compatibility */
/* Include responsive breakpoints at 768px and 480px */
</style>

<div class="my-root">
  <!-- Page content with bilingual text -->
</div>

<script>
(function() {
  // Interactivity logic (tab switching, filters, etc.)
})();
</script>

Design system

Typography

The app uses these fonts (already loaded globally):

Font Usage
'Be Vietnam Pro' Body text, UI elements, labels — the primary font
'Playfair Display' Display headings, editorial titles
'JetBrains Mono' / 'SF Mono' / 'Fira Code' Monospace numbers, data values

Common patterns

Section title:

<div class="section-title" style="color: #2563EB">
  <span data-lang="vi">Tiêu đề phần</span><span data-lang="en">Section Title</span>
</div>
.section-title {
  font-family: 'Be Vietnam Pro', sans-serif;
  font-size: 14px;
  font-weight: 700;
  letter-spacing: 0.06em;
  text-transform: uppercase;
  margin-bottom: 12px;
}

Card:

.card {
  background: var(--card-bg);
  border: 1px solid var(--border-color);
  padding: 16px;
  margin-bottom: 12px;
}

Callout / assessment box:

.callout {
  background: var(--surface-2);
  border-left: 3px solid #2c6fbb;
  padding: 16px 20px;
  font-size: 13px;
  line-height: 1.75;
  color: var(--text-secondary);
}

Metric chip (small info badge):

.chip {
  display: inline-flex;
  align-items: center;
  gap: 6px;
  font-size: 0.78rem;
  color: var(--text-tertiary);
  padding: 5px 10px;
  border: 1px solid var(--border-color);
  border-radius: 2px;
}
.chip strong { color: var(--text-color); font-weight: 600; }

Tab bar:

.tabs { display: flex; gap: 4px; flex-wrap: wrap; margin-bottom: 16px; }
.tab {
  background: transparent;
  border: 1px solid var(--border-color);
  padding: 6px 14px;
  font-size: 11px;
  font-weight: 600;
  color: var(--text-secondary);
  cursor: pointer;
  transition: all 0.15s;
}
.tab.active {
  background: var(--text-color);
  color: var(--bg-color);
  border-color: var(--text-color);
}

Tooltip (CSS-only hover):

<span class="ttip-anchor">
  <span class="ttip-icon">?</span>
  <span class="ttip-popup">Tooltip text here</span>
</span>
.ttip-anchor { position: relative; display: inline-block; cursor: help; }
.ttip-icon {
  display: inline-flex; align-items: center; justify-content: center;
  width: 14px; height: 14px; font-size: 9px; font-weight: 700;
  border-radius: 50%; border: 1px solid var(--border-color);
  color: var(--text-tertiary); background: var(--surface-2);
}
.ttip-popup {
  display: none; position: absolute; bottom: calc(100% + 8px); left: 50%;
  transform: translateX(-50%); background: var(--text-color); color: var(--bg-color);
  padding: 8px 12px; font-size: 11px; line-height: 1.5; white-space: nowrap;
  border-radius: 4px; z-index: 100; pointer-events: none;
}
.ttip-anchor:hover .ttip-popup { display: block; }

Crisis badge with pulse animation:

.pulse-dot {
  width: 8px; height: 8px; border-radius: 50%; background: #DC2626;
  animation: pulse 1.5s ease-in-out infinite;
}
@keyframes pulse {
  0%, 100% { opacity: 1; transform: scale(1); }
  50% { opacity: 0.4; transform: scale(0.7); }
}

Data table:

.data-table { width: 100%; border-collapse: collapse; font-size: 12px; }
.data-table th {
  text-align: left; padding: 8px 10px; font-weight: 600;
  color: var(--text-secondary); border-bottom: 2px solid var(--border-color);
  font-size: 10px; text-transform: uppercase; letter-spacing: 0.06em;
}
.data-table td {
  padding: 8px 10px; border-bottom: 1px solid var(--border-color);
  color: var(--text-color);
}
.data-table tr:hover { background: var(--hover-bg); }

Filter buttons:

.filter-btn {
  padding: 5px 12px; font-size: 10px; font-weight: 600;
  background: transparent; border: 1px solid var(--border-color);
  color: var(--text-secondary); cursor: pointer;
}
.filter-btn.active {
  background: var(--text-color); color: var(--bg-color);
  border-color: var(--text-color);
}

Responsive breakpoints

Always include responsive styles:

@media (max-width: 768px) {
  /* Tablet: stack grids to 1-2 columns, reduce padding */
}
@media (max-width: 480px) {
  /* Mobile: single column, smaller fonts, collapse filters */
}

Tab switching template

For pages with multiple tabs:

<div class="tabs" role="tablist">
  <button class="tab active" data-tab-key="overview" onclick="window.__mySwitch('overview')" role="tab" aria-selected="true">
    <span data-lang="vi">Tổng Quan</span><span data-lang="en">Overview</span>
  </button>
  <button class="tab" data-tab-key="details" onclick="window.__mySwitch('details')" role="tab" aria-selected="false">
    <span data-lang="vi">Chi Tiết</span><span data-lang="en">Details</span>
  </button>
</div>

<div class="tab-content" data-tab="overview">
  <!-- Overview content -->
</div>
<div class="tab-content" data-tab="details" style="display:none">
  <!-- Details content -->
</div>

<script>
(function() {
  function switchTab(key) {
    document.querySelectorAll('.tab-content').forEach(function(el) {
      el.style.display = el.getAttribute('data-tab') === key ? '' : 'none';
    });
    document.querySelectorAll('.tab').forEach(function(el) {
      var isActive = el.getAttribute('data-tab-key') === key;
      el.classList.toggle('active', isActive);
      el.setAttribute('aria-selected', isActive ? 'true' : 'false');
    });
  }
  window.__mySwitch = switchTab;
})();
</script>

Accordion/expand template

<div class="accordion-item">
  <div class="accordion-header" onclick="this.parentElement.classList.toggle('open')">
    <span>Title</span>
    <span class="toggle-icon">+</span>
  </div>
  <div class="accordion-body">
    <p>Expandable content here</p>
  </div>
</div>
.accordion-body { display: none; }
.accordion-item.open .accordion-body { display: block; }
.accordion-item.open .toggle-icon::after { content: '−'; }

Checklist before saving

  • No <html>, <head>, or <body> tags
  • All CSS in a single <style> block at the top
  • All JS in a <script> block at the bottom, wrapped in IIFE
  • All translatable text uses <span data-lang="vi"> / <span data-lang="en"> pairs
  • Shared content (numbers, names, dates) is NOT wrapped in data-lang
  • All colors use CSS variables (var(--text-color), etc.) — no hardcoded black/white
  • Dark mode overrides use [data-theme="dark"] .class { ... } where needed
  • Responsive styles at 768px and 480px breakpoints
  • Interactive functions use window.__uniquePrefix namespace
  • Font families: 'Be Vietnam Pro' for body, 'Playfair Display' for headings
  • border-radius: 0 or 2px (the app uses sharp/minimal corners)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment