Last active
March 24, 2026 19:22
-
-
Save beacrea/62df2bad7000606525ce518a59e99268 to your computer and use it in GitHub Desktop.
Button Shape Ratio — Interactive Sandbox (DSYS)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!DOCTYPE html> | |
| <html lang="en" data-theme="light"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>Button Shape Ratio — Interactive Sandbox</title> | |
| <style> | |
| *, | |
| *::before, | |
| *::after { | |
| box-sizing: border-box; | |
| margin: 0; | |
| padding: 0; | |
| } | |
| :root { | |
| --bg: #ffffff; | |
| --bg-alt: #f6f7f9; | |
| --bg-card: #ffffff; | |
| --bg-code: #f0f1f3; | |
| --fg: #1a1a2e; | |
| --fg-muted: #6b7280; | |
| --fg-accent: #4f46e5; | |
| --border: #e5e7eb; | |
| --border-focus: #4f46e5; | |
| --green: #059669; | |
| --amber: #d97706; | |
| --red: #dc2626; | |
| --shadow: 0 1px 3px rgba(0, 0, 0, .08); | |
| --font: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; | |
| --mono: 'JetBrains Mono', 'Fira Code', monospace; | |
| --btn-bg: #4f46e5; | |
| --btn-fg: #fff; | |
| } | |
| [data-theme="dark"] { | |
| --bg: #0f1117; | |
| --bg-alt: #1a1d2e; | |
| --bg-card: #1e2132; | |
| --bg-code: #252840; | |
| --fg: #e2e8f0; | |
| --fg-muted: #94a3b8; | |
| --fg-accent: #818cf8; | |
| --border: #2d3148; | |
| --border-focus: #818cf8; | |
| --shadow: 0 1px 3px rgba(0, 0, 0, .3); | |
| --btn-bg: #6366f1; | |
| --btn-fg: #fff; | |
| } | |
| html { | |
| scroll-behavior: smooth; | |
| } | |
| body { | |
| font-family: var(--font); | |
| background: var(--bg); | |
| color: var(--fg); | |
| line-height: 1.6; | |
| font-size: 16px; | |
| transition: background .2s, color .2s; | |
| } | |
| .container { | |
| max-width: 1100px; | |
| margin: 0 auto; | |
| padding: 0 24px; | |
| } | |
| header { | |
| padding: 48px 0 32px; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| section { | |
| padding: 40px 0; | |
| border-bottom: 1px solid var(--border); | |
| } | |
| section:last-of-type { | |
| border-bottom: none; | |
| } | |
| h1 { | |
| font-size: 1.75rem; | |
| font-weight: 700; | |
| letter-spacing: -0.02em; | |
| margin-bottom: 4px; | |
| } | |
| h2 { | |
| font-size: 1.25rem; | |
| font-weight: 600; | |
| letter-spacing: -0.01em; | |
| margin-bottom: 16px; | |
| } | |
| .subtitle { | |
| font-size: 0.95rem; | |
| color: var(--fg-muted); | |
| margin-bottom: 0; | |
| } | |
| code { | |
| font-family: var(--mono); | |
| font-size: 0.82em; | |
| background: var(--bg-code); | |
| padding: 2px 6px; | |
| border-radius: 4px; | |
| } | |
| nav { | |
| position: sticky; | |
| top: 0; | |
| z-index: 100; | |
| border-bottom: 1px solid var(--border); | |
| padding: 10px 0; | |
| backdrop-filter: blur(12px); | |
| background: color-mix(in srgb, var(--bg) 85%, transparent); | |
| } | |
| nav .container { | |
| display: flex; | |
| align-items: center; | |
| gap: 12px; | |
| } | |
| nav span { | |
| font-size: 0.8rem; | |
| font-weight: 600; | |
| color: var(--fg-muted); | |
| } | |
| .nav-spacer { | |
| flex: 1; | |
| } | |
| #theme-toggle { | |
| background: none; | |
| border: 1px solid var(--border); | |
| border-radius: 6px; | |
| padding: 4px 10px; | |
| cursor: pointer; | |
| font-size: 0.8rem; | |
| color: var(--fg-muted); | |
| } | |
| #theme-toggle:hover { | |
| border-color: var(--fg-muted); | |
| } | |
| /* ─── Controls ─── */ | |
| .controls { | |
| display: flex; | |
| flex-wrap: wrap; | |
| gap: 24px; | |
| align-items: end; | |
| padding: 20px; | |
| background: var(--bg-alt); | |
| border-radius: 10px; | |
| border: 1px solid var(--border); | |
| margin-bottom: 32px; | |
| } | |
| .control-group { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 6px; | |
| } | |
| .control-group label { | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--fg-muted); | |
| } | |
| .control-group input[type="range"] { | |
| width: 240px; | |
| accent-color: var(--fg-accent); | |
| } | |
| .ratio-display { | |
| font-family: var(--mono); | |
| font-size: 1.5rem; | |
| font-weight: 700; | |
| color: var(--fg-accent); | |
| min-width: 4ch; | |
| min-width: 80px; | |
| } | |
| .toggle-group { | |
| display: flex; | |
| gap: 4px; | |
| } | |
| .toggle-btn { | |
| font-size: 0.75rem; | |
| font-weight: 600; | |
| padding: 5px 12px; | |
| border-radius: 6px; | |
| border: 1px solid var(--border); | |
| background: var(--bg); | |
| color: var(--fg-muted); | |
| cursor: pointer; | |
| transition: all .15s; | |
| } | |
| .toggle-btn.active { | |
| background: var(--fg-accent); | |
| color: #fff; | |
| border-color: var(--fg-accent); | |
| } | |
| .toggle-btn:hover:not(.active) { | |
| border-color: var(--fg-muted); | |
| } | |
| /* ─── Vertical Comparison ─── */ | |
| .comparison-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| border: 1px solid var(--border); | |
| border-radius: 10px; | |
| overflow: hidden; | |
| } | |
| .comparison-table th { | |
| background: var(--bg-alt); | |
| font-size: 0.7rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--fg-muted); | |
| padding: 10px 16px; | |
| border-bottom: 1px solid var(--border); | |
| text-align: left; | |
| } | |
| .comparison-table th.col-current { | |
| text-align: right; | |
| } | |
| .comparison-table th.col-ratio { | |
| text-align: left; | |
| } | |
| .comparison-table td { | |
| border-bottom: 1px solid var(--border); | |
| vertical-align: middle; | |
| } | |
| .comparison-table tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .size-meta { | |
| padding: 20px 16px; | |
| background: var(--bg-alt); | |
| width: 100px; | |
| border-right: 1px solid var(--border); | |
| } | |
| .size-name { | |
| font-size: 1rem; | |
| font-weight: 700; | |
| color: var(--fg); | |
| } | |
| .size-detail { | |
| font-family: var(--mono); | |
| font-size: 0.7rem; | |
| color: var(--fg-muted); | |
| margin-top: 2px; | |
| line-height: 1.4; | |
| } | |
| .btn-cell { | |
| padding: 20px 24px; | |
| text-align: right; | |
| } | |
| .btn-cell .specimen-meta { | |
| text-align: right; | |
| } | |
| .btn-cell-ratio { | |
| border-left: 1px dashed var(--border); | |
| text-align: left; | |
| } | |
| .btn-cell-ratio .specimen-meta { | |
| text-align: left; | |
| } | |
| .btn-specimen { | |
| display: inline-flex; | |
| align-items: center; | |
| justify-content: center; | |
| background: var(--btn-bg); | |
| color: var(--btn-fg); | |
| font-weight: 600; | |
| white-space: nowrap; | |
| border: none; | |
| transition: border-radius .1s ease; | |
| line-height: 1; | |
| } | |
| .btn-specimen svg { | |
| flex-shrink: 0; | |
| } | |
| .specimen-meta { | |
| font-family: var(--mono); | |
| font-size: 0.65rem; | |
| color: var(--fg-muted); | |
| margin-top: 8px; | |
| line-height: 1.4; | |
| } | |
| .specimen-meta .val { | |
| font-weight: 700; | |
| color: var(--fg); | |
| } | |
| .specimen-meta .delta { | |
| font-weight: 600; | |
| } | |
| .delta-pos { | |
| color: var(--green); | |
| } | |
| .delta-neg { | |
| color: var(--red); | |
| } | |
| .delta-zero { | |
| color: var(--fg-muted); | |
| } | |
| /* ─── Formula Preview ─── */ | |
| .formula-preview { | |
| background: var(--bg-code); | |
| border-radius: 8px; | |
| padding: 16px 20px; | |
| font-family: var(--mono); | |
| font-size: 0.8rem; | |
| line-height: 1.8; | |
| overflow-x: auto; | |
| margin-top: 24px; | |
| border: 1px solid var(--border); | |
| } | |
| .formula-line { | |
| white-space: pre; | |
| } | |
| .token-ref { | |
| color: var(--fg-accent); | |
| } | |
| .computed { | |
| color: var(--green); | |
| font-weight: 600; | |
| } | |
| .formula-header { | |
| margin-bottom: 8px; | |
| color: var(--fg-muted); | |
| font-size: 0.7rem; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| } | |
| /* ─── Summary Table ─── */ | |
| .summary-table { | |
| width: 100%; | |
| border-collapse: collapse; | |
| font-size: 0.82rem; | |
| margin-top: 16px; | |
| } | |
| .summary-table th { | |
| text-align: left; | |
| font-size: 0.7rem; | |
| font-weight: 600; | |
| text-transform: uppercase; | |
| letter-spacing: 0.05em; | |
| color: var(--fg-muted); | |
| padding: 8px 12px; | |
| border-bottom: 2px solid var(--border); | |
| } | |
| .summary-table td { | |
| padding: 8px 12px; | |
| border-bottom: 1px solid var(--border); | |
| font-family: var(--mono); | |
| font-size: 0.8rem; | |
| } | |
| .summary-table tr:last-child td { | |
| border-bottom: none; | |
| } | |
| .match { | |
| color: var(--green); | |
| font-weight: 600; | |
| } | |
| .diff { | |
| color: var(--amber); | |
| font-weight: 600; | |
| } | |
| </style> | |
| </head> | |
| <body> | |
| <nav> | |
| <div class="container"> | |
| <span>Button Shape Ratio Sandbox</span> | |
| <span style="color:var(--fg-muted);font-size:0.75rem">ratio-based border-radius exploration</span> | |
| <div class="nav-spacer"></div> | |
| <button id="theme-toggle">Toggle theme</button> | |
| </div> | |
| </nav> | |
| <div class="container"> | |
| <header> | |
| <h1>Shape Ratio Explorer</h1> | |
| <p class="subtitle"> | |
| <code>border-radius = round(fontSize × ratio, snapGrid)</code> | |
| — replacing hand-tuned shape tokens with a single ratio. | |
| </p> | |
| </header> | |
| <section> | |
| <div class="controls"> | |
| <div class="control-group"> | |
| <label>Shape Ratio</label> | |
| <input type="range" id="ratio-slider" min="0.30" max="1.00" step="0.01" value="0.45"> | |
| </div> | |
| <div class="control-group"> | |
| <label> </label> | |
| <span class="ratio-display" id="ratio-value">0.45</span> | |
| </div> | |
| <div class="control-group"> | |
| <label>Snap Grid</label> | |
| <div class="toggle-group" id="snap-toggles"> | |
| <button class="toggle-btn" data-snap="0">None</button> | |
| <button class="toggle-btn active" data-snap="1">1px</button> | |
| <button class="toggle-btn" data-snap="2">2px</button> | |
| <button class="toggle-btn" data-snap="4">4px</button> | |
| </div> | |
| </div> | |
| <div class="control-group"> | |
| <label>Presets</label> | |
| <div class="toggle-group" id="preset-toggles"> | |
| <button class="toggle-btn" data-ratio="0.45">0.45</button> | |
| <button class="toggle-btn" data-ratio="0.50">0.50</button> | |
| <button class="toggle-btn" data-ratio="0.55">0.55</button> | |
| <button class="toggle-btn active" data-ratio="0.60">0.60</button> | |
| <button class="toggle-btn" data-ratio="0.667">2:3</button> | |
| </div> | |
| </div> | |
| </div> | |
| <h2>Visual Comparison</h2> | |
| <table class="comparison-table" id="comparison-table"> | |
| <thead> | |
| <tr> | |
| <th>Size</th> | |
| <th class="col-current">Current (hand-tuned)</th> | |
| <th class="col-ratio">Ratio-based</th> | |
| </tr> | |
| </thead> | |
| <tbody id="comparison-body"></tbody> | |
| </table> | |
| <div class="formula-preview" id="formula-preview"></div> | |
| </section> | |
| <section> | |
| <h2>Value Summary</h2> | |
| <table class="summary-table"> | |
| <thead> | |
| <tr> | |
| <th>Size</th> | |
| <th>Typescale</th> | |
| <th>Font px</th> | |
| <th>Current Token</th> | |
| <th>Current px</th> | |
| <th>Raw Computed</th> | |
| <th>Snapped</th> | |
| <th>Delta</th> | |
| </tr> | |
| </thead> | |
| <tbody id="summary-body"></tbody> | |
| </table> | |
| </section> | |
| </div> | |
| <script> | |
| var SCALE_RATIO = 1.067; | |
| var BASE_PX = 16; | |
| function typescalePx(step) { | |
| return BASE_PX * Math.pow(SCALE_RATIO, step - 12); | |
| } | |
| // All geometry from canonical button tokens | |
| var SIZES = [ | |
| { name: 'xs', step: 10, currentToken: 'shape.md-lg', currentPx: 6, padX: 12, padY: 6, gapIcon: 2 }, | |
| { name: 'sm', step: 12, currentToken: 'shape.lg', currentPx: 8, padX: 14, padY: 8, gapIcon: 4 }, | |
| { name: 'md', step: 15, currentToken: 'shape.xl', currentPx: 12, padX: 16, padY: 10, gapIcon: 4 }, | |
| { name: 'lg', step: 18, currentToken: 'shape.xl', currentPx: 12, padX: 20, padY: 12, gapIcon: 6 }, | |
| { name: 'xl', step: 21, currentToken: 'shape.2xl', currentPx: 16, padX: 20, padY: 12, gapIcon: 8 }, | |
| { name: '2xl', step: 23, currentToken: 'shape.2xl', currentPx: 16, padX: 24, padY: 12, gapIcon: 10 }, | |
| { name: '3xl', step: 26, currentToken: 'shape.2xl', currentPx: 16, padX: 24, padY: 12, gapIcon: 10 }, | |
| ]; | |
| SIZES.forEach(function (s) { s.fontPx = typescalePx(s.step); }); | |
| var ratio = 0.60; | |
| var snapGrid = 1; | |
| function snap(value, grid) { | |
| if (grid === 0) return value; | |
| return Math.round(value / grid) * grid; | |
| } | |
| function computeRadius(size) { | |
| var raw = size.fontPx * ratio; | |
| var snapped = snap(raw, snapGrid); | |
| return { raw: raw, snapped: snapped }; | |
| } | |
| // ─── SVG icon factories ─── | |
| // Lucide icon factory — stroke-based, no fill | |
| function makeLucideIcon(size, paths) { | |
| var svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); | |
| svg.setAttribute('width', String(Math.round(size))); | |
| svg.setAttribute('height', String(Math.round(size))); | |
| svg.setAttribute('viewBox', '0 0 24 24'); | |
| svg.setAttribute('fill', 'none'); | |
| svg.setAttribute('stroke', 'currentColor'); | |
| svg.setAttribute('stroke-width', '2'); | |
| svg.setAttribute('stroke-linecap', 'round'); | |
| svg.setAttribute('stroke-linejoin', 'round'); | |
| svg.style.flexShrink = '0'; | |
| paths.forEach(function (d) { | |
| var path = document.createElementNS('http://www.w3.org/2000/svg', 'path'); | |
| path.setAttribute('d', d); | |
| svg.appendChild(path); | |
| }); | |
| return svg; | |
| } | |
| // Lucide "star" — leading icon | |
| function makeLeadingIcon(size) { | |
| return makeLucideIcon(size, [ | |
| 'M11.525 2.295a.53.53 0 0 1 .95 0l2.31 4.679a2.123 2.123 0 0 0 1.595 1.16l5.166.756a.53.53 0 0 1 .294.904l-3.736 3.638a2.123 2.123 0 0 0-.611 1.878l.882 5.14a.53.53 0 0 1-.771.56l-4.618-2.428a2.122 2.122 0 0 0-1.973 0L6.396 21.01a.53.53 0 0 1-.77-.56l.881-5.139a2.122 2.122 0 0 0-.611-1.879L2.16 9.795a.53.53 0 0 1 .294-.906l5.165-.755a2.122 2.122 0 0 0 1.597-1.16z' | |
| ]); | |
| } | |
| // Lucide "chevron-right" — trailing icon | |
| function makeTrailingIcon(size) { | |
| return makeLucideIcon(size, [ | |
| 'm9 18 6-6-6-6' | |
| ]); | |
| } | |
| // ─── Build a button specimen ─── | |
| function makeButton(s, borderRadius) { | |
| var btn = document.createElement('div'); | |
| btn.className = 'btn-specimen'; | |
| btn.style.fontSize = s.fontPx + 'px'; | |
| btn.style.borderRadius = borderRadius + 'px'; | |
| btn.style.padding = s.padY + 'px ' + s.padX + 'px'; | |
| btn.style.gap = s.gapIcon + 'px'; | |
| btn.appendChild(makeLeadingIcon(s.fontPx)); | |
| var label = document.createElement('span'); | |
| label.textContent = 'Click me'; | |
| btn.appendChild(label); | |
| btn.appendChild(makeTrailingIcon(s.fontPx)); | |
| return btn; | |
| } | |
| // ─── Render comparison ─── | |
| function renderComparison() { | |
| var tbody = document.getElementById('comparison-body'); | |
| tbody.replaceChildren(); | |
| SIZES.forEach(function (s) { | |
| var result = computeRadius(s); | |
| var raw = result.raw; | |
| var snapped = result.snapped; | |
| var delta = snapped - s.currentPx; | |
| var deltaClass = delta > 0 ? 'delta-pos' : delta < 0 ? 'delta-neg' : 'delta-zero'; | |
| var deltaStr = delta > 0 ? ('+' + delta) : delta === 0 ? '0' : String(delta); | |
| var tr = document.createElement('tr'); | |
| // Size meta cell | |
| var tdMeta = document.createElement('td'); | |
| tdMeta.className = 'size-meta'; | |
| var nameDiv = document.createElement('div'); | |
| nameDiv.className = 'size-name'; | |
| nameDiv.textContent = s.name; | |
| tdMeta.appendChild(nameDiv); | |
| var detailDiv = document.createElement('div'); | |
| detailDiv.className = 'size-detail'; | |
| detailDiv.textContent = 'ts-' + s.step + ' \u2022 ' + s.fontPx.toFixed(1) + 'px'; | |
| tdMeta.appendChild(detailDiv); | |
| tr.appendChild(tdMeta); | |
| // Current button cell | |
| var tdCurrent = document.createElement('td'); | |
| tdCurrent.className = 'btn-cell'; | |
| tdCurrent.appendChild(makeButton(s, s.currentPx)); | |
| var metaCurrent = document.createElement('div'); | |
| metaCurrent.className = 'specimen-meta'; | |
| var valSpan = document.createElement('span'); | |
| valSpan.className = 'val'; | |
| valSpan.textContent = s.currentPx + 'px'; | |
| metaCurrent.appendChild(valSpan); | |
| metaCurrent.appendChild(document.createTextNode(' \u2022 ' + s.currentToken)); | |
| tdCurrent.appendChild(metaCurrent); | |
| tr.appendChild(tdCurrent); | |
| // Ratio button cell | |
| var tdRatio = document.createElement('td'); | |
| tdRatio.className = 'btn-cell btn-cell-ratio'; | |
| tdRatio.appendChild(makeButton(s, snapped)); | |
| var metaRatio = document.createElement('div'); | |
| metaRatio.className = 'specimen-meta'; | |
| var valSpan2 = document.createElement('span'); | |
| valSpan2.className = 'val'; | |
| valSpan2.textContent = snapped + 'px'; | |
| metaRatio.appendChild(valSpan2); | |
| var rawSpan = document.createTextNode(' (' + raw.toFixed(1) + ') '); | |
| metaRatio.appendChild(rawSpan); | |
| var deltaSpan = document.createElement('span'); | |
| deltaSpan.className = 'delta ' + deltaClass; | |
| deltaSpan.textContent = deltaStr + 'px'; | |
| metaRatio.appendChild(deltaSpan); | |
| tdRatio.appendChild(metaRatio); | |
| tr.appendChild(tdRatio); | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| // ─── Render formula preview ─── | |
| function renderFormula() { | |
| var container = document.getElementById('formula-preview'); | |
| container.replaceChildren(); | |
| var snapLabel = snapGrid === 0 ? 'none' : snapGrid + 'px'; | |
| var header = document.createElement('div'); | |
| header.className = 'formula-header'; | |
| header.textContent = 'Formula: math({shape.ratio.button} * {typography.scale.typescale-N}) | snap: ' + snapLabel; | |
| container.appendChild(header); | |
| SIZES.forEach(function (s) { | |
| var result = computeRadius(s); | |
| var raw = result.raw; | |
| var snapped = result.snapped; | |
| var line = document.createElement('div'); | |
| line.className = 'formula-line'; | |
| var ref1 = document.createElement('span'); | |
| ref1.className = 'token-ref'; | |
| ref1.textContent = '{shape.ratio.button}'; | |
| line.appendChild(ref1); | |
| line.appendChild(document.createTextNode(' (' + ratio + ') \u00d7 ')); | |
| var ref2 = document.createElement('span'); | |
| ref2.className = 'token-ref'; | |
| ref2.textContent = '{typography.scale.typescale-' + s.step + '}'; | |
| line.appendChild(ref2); | |
| line.appendChild(document.createTextNode(' (' + s.fontPx.toFixed(2) + 'px) = ' + raw.toFixed(2))); | |
| if (snapGrid > 0) { | |
| line.appendChild(document.createTextNode(' \u2192 snap(' + snapGrid + ') \u2192 ')); | |
| } else { | |
| line.appendChild(document.createTextNode(' \u2192 ')); | |
| } | |
| var comp = document.createElement('span'); | |
| comp.className = 'computed'; | |
| comp.textContent = (snapGrid > 0 ? snapped : raw.toFixed(2)) + 'px'; | |
| line.appendChild(comp); | |
| line.appendChild(document.createTextNode(' // ' + s.name)); | |
| container.appendChild(line); | |
| }); | |
| } | |
| // ─── Render summary table ─── | |
| function renderSummary() { | |
| var tbody = document.getElementById('summary-body'); | |
| tbody.replaceChildren(); | |
| SIZES.forEach(function (s) { | |
| var result = computeRadius(s); | |
| var raw = result.raw; | |
| var snapped = result.snapped; | |
| var delta = snapped - s.currentPx; | |
| var deltaClass = delta === 0 ? 'match' : 'diff'; | |
| var deltaStr = delta > 0 ? ('+' + delta + 'px') : delta === 0 ? '0' : (delta + 'px'); | |
| var tr = document.createElement('tr'); | |
| var cells = [ | |
| { text: s.name, style: 'font-weight:600;color:var(--fg)' }, | |
| { text: 'typescale-' + s.step }, | |
| { text: s.fontPx.toFixed(2) }, | |
| { text: s.currentToken, style: 'color:var(--fg-muted)' }, | |
| { text: String(s.currentPx) }, | |
| { text: raw.toFixed(2) }, | |
| { text: String(snapped), style: 'font-weight:700' }, | |
| { text: deltaStr, cls: deltaClass }, | |
| ]; | |
| cells.forEach(function (c) { | |
| var td = document.createElement('td'); | |
| td.textContent = c.text; | |
| if (c.style) td.setAttribute('style', c.style); | |
| if (c.cls) td.className = c.cls; | |
| tr.appendChild(td); | |
| }); | |
| tbody.appendChild(tr); | |
| }); | |
| } | |
| function render() { | |
| var display = ratio === 0.667 ? ratio.toFixed(3) : ratio.toFixed(2); | |
| document.getElementById('ratio-value').textContent = display; | |
| renderComparison(); | |
| renderFormula(); | |
| renderSummary(); | |
| } | |
| // ─── Event handlers ─── | |
| document.getElementById('ratio-slider').addEventListener('input', function (e) { | |
| ratio = parseFloat(e.target.value); | |
| document.querySelectorAll('#preset-toggles .toggle-btn').forEach(function (b) { | |
| b.classList.toggle('active', parseFloat(b.dataset.ratio) === ratio); | |
| }); | |
| render(); | |
| }); | |
| document.getElementById('snap-toggles').addEventListener('click', function (e) { | |
| var btn = e.target.closest('.toggle-btn'); | |
| if (!btn) return; | |
| snapGrid = parseInt(btn.dataset.snap); | |
| document.querySelectorAll('#snap-toggles .toggle-btn').forEach(function (b) { b.classList.remove('active'); }); | |
| btn.classList.add('active'); | |
| render(); | |
| }); | |
| document.getElementById('preset-toggles').addEventListener('click', function (e) { | |
| var btn = e.target.closest('.toggle-btn'); | |
| if (!btn) return; | |
| ratio = parseFloat(btn.dataset.ratio); | |
| document.getElementById('ratio-slider').value = ratio; | |
| document.querySelectorAll('#preset-toggles .toggle-btn').forEach(function (b) { b.classList.remove('active'); }); | |
| btn.classList.add('active'); | |
| render(); | |
| }); | |
| document.getElementById('theme-toggle').addEventListener('click', function () { | |
| var html = document.documentElement; | |
| html.dataset.theme = html.dataset.theme === 'dark' ? 'light' : 'dark'; | |
| }); | |
| render(); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment