mirror of
https://github.com/nexu-io/open-design.git
synced 2026-09-20 06:15:06 +08:00
f1a0b60c6c
* chore: unify product name display to OpenDesign repo-wide Rename the user-facing product name from "Open Design" to "OpenDesign" across documentation, web/desktop UI strings, i18n locales, prompts, plugin/skill/design-system content, changelogs, and specs, matching the landing-page rename already in flight. Deliberately excluded so installed-app identity stays untouched: - packages/release PRODUCT_NAME and everything derived from it (app bundle names, installer/registry identity, updater feeds) - tools/pack, tools/release, apps/packaged, landing page (PR #6983) - mocks/ recorded traces and LICENSE - test fixtures and comments that encode real on-disk install paths (Open Design.app, Program Files\Open Design, AppData paths) Validated with pnpm guard, pnpm typecheck, and targeted web/desktop/host test runs over the renamed strings. * fix(web): keep two-letter author monogram for CamelCase brand names authorInitials split words on whitespace only, so the renamed single-token author name "OpenDesign" produced a one-letter monogram. Treat lower-to-upper CamelCase boundaries as word breaks so the avatar fallback stays "OD". * fix(release): restore spaced Windows source artifact names in win.ps1 tools/pack still derives builder outputs from the unchanged PRODUCT_NAME ("Open Design"), so the asset script must look for the spaced filenames. Matches the mac/mac-intel/linux asset scripts, which were already kept on the original names. --------- Co-authored-by: Joey <236967869+joeylee12629-star@users.noreply.github.com>
172 lines
9.0 KiB
HTML
172 lines
9.0 KiB
HTML
<!doctype html>
|
|
<html lang="en">
|
|
<head>
|
|
<meta charset="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<title>Write an Operating Review like a Disciplined COO - Corporate strategy</title>
|
|
<style>
|
|
:root {
|
|
--bg: #fafaf9; --fg: #1c1b1a; --muted: #6b6964; --accent: #c96442; --surface: #ffffff;
|
|
}
|
|
* { box-sizing: border-box; }
|
|
html, body { margin: 0; height: 100%; }
|
|
body {
|
|
background: var(--bg);
|
|
color: var(--fg);
|
|
font: 18px/1.5 -apple-system, system-ui, sans-serif;
|
|
display: flex;
|
|
overflow-x: auto;
|
|
overflow-y: hidden;
|
|
scroll-snap-type: x mandatory;
|
|
scroll-behavior: smooth;
|
|
}
|
|
body::-webkit-scrollbar { display: none; }
|
|
.slide {
|
|
flex: 0 0 100vw;
|
|
height: 100vh;
|
|
scroll-snap-align: start;
|
|
padding: 80px 96px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
justify-content: center;
|
|
position: relative;
|
|
}
|
|
.slide.title { background: var(--fg); color: var(--bg); }
|
|
.eyebrow { font-size: 12px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--accent); margin-bottom: 28px; }
|
|
.slide h1 { font-size: clamp(48px, 7vw, 96px); line-height: 1.05; letter-spacing: -0.025em; margin: 0 0 20px; max-width: 16ch; }
|
|
.slide h2 { font-size: clamp(32px, 4vw, 48px); letter-spacing: -0.015em; margin: 0 0 20px; max-width: 20ch; }
|
|
.slide .body { font-size: 22px; color: var(--muted); max-width: 56ch; }
|
|
.slide.title .body { color: rgba(250,250,249,0.7); }
|
|
.slide.big-stat .number { font-size: clamp(120px, 22vw, 280px); line-height: 0.9; letter-spacing: -0.04em; color: var(--accent); margin-bottom: 16px; font-weight: 600; }
|
|
.slide.big-stat .caption { font-size: 24px; color: var(--muted); max-width: 24ch; }
|
|
.quote-mark { font-family: Georgia, serif; font-size: 200px; line-height: 0.7; color: var(--accent); opacity: 0.18; margin-bottom: -40px; }
|
|
.quote-text { font-family: Georgia, serif; font-size: 36px; line-height: 1.3; max-width: 26ch; margin: 0 0 28px; }
|
|
.quote-author { font-size: 14px; color: var(--muted); }
|
|
.grid-3 { display: grid; grid-template-columns: repeat(3, 1fr); gap: 32px; margin-top: 40px; }
|
|
.grid-3 .pt { border-top: 2px solid var(--accent); padding-top: 16px; }
|
|
.grid-3 .pt .h { font-size: 18px; font-weight: 500; margin: 0 0 8px; }
|
|
.grid-3 .pt .p { color: var(--muted); margin: 0; font-size: 16px; }
|
|
.counter { position: fixed; bottom: 24px; right: 32px; font-family: ui-monospace, monospace; font-size: 12px; color: var(--muted); background: var(--surface); padding: 4px 10px; border-radius: 999px; border: 1px solid #e6e4e0; }
|
|
.hint { position: fixed; bottom: 24px; left: 32px; font-size: 11px; color: var(--muted); }
|
|
</style>
|
|
</head>
|
|
<body>
|
|
<section class="slide title" data-od-id="slide-1">
|
|
<div class="eyebrow" style="color:#c96442;">Corporate Strategy · Operating Review · Board Pre-Read</div>
|
|
<h1>Approve the plan: cut burn 35%, protect the open core.</h1>
|
|
<p class="body">OpenDesign's Q2 operating review — growth, burn, and the concrete path to sustainability, without gating the Apache-2.0 project that got us here.</p>
|
|
</section>
|
|
<section class="slide" data-od-id="slide-2">
|
|
<div class="eyebrow">Recommendation</div>
|
|
<h2>Extend runway to 22 months without touching the open core.</h2>
|
|
<p class="body">Growth is not the problem — 76K GitHub stars and 200+ community plugins prove demand. The gap is cost structure: we're subsidizing every user's model calls. The fix is to make BYOK the default, license self-hosted enterprise deployments, and leave the open-source desktop app untouched.</p>
|
|
</section>
|
|
<section class="slide big-stat" data-od-id="slide-3">
|
|
<div class="number">42%</div>
|
|
<div class="caption">Cloud inference spend cut, from $187K to $109K/month, since local-first agent routing shipped in Q1.</div>
|
|
</section>
|
|
<section class="slide" data-od-id="slide-4">
|
|
<div class="eyebrow">Options considered</div>
|
|
<h2>Three paths to sustainability.</h2>
|
|
<div class="grid-3">
|
|
<div class="pt"><h3 class="h">A — Hold course</h3><p class="p">Keep subsidizing hosted inference. Burn stays flat at $187K/mo; runway closes to 9 months by Q4.</p></div>
|
|
<div class="pt"><h3 class="h">B — BYOK default (recommended)</h3><p class="p">Users bring their own model keys by default. Cuts hosted inference cost ~40% with zero change to the open-source app.</p></div>
|
|
<div class="pt"><h3 class="h">C — Enterprise self-host license</h3><p class="p">Charge for managed self-hosted deployments and support, on top of B, without paywalling any existing feature.</p></div>
|
|
</div>
|
|
</section>
|
|
<section class="slide" data-od-id="slide-5">
|
|
<div class="quote-mark">"</div>
|
|
<p class="quote-text">If we gate any existing plugin or feature behind a paywall to hit these numbers, we break the open-source flywheel that built the 200+ plugin ecosystem — that risk isn't worth the savings.</p>
|
|
<p class="quote-author">— Risk flagged jointly by Engineering and Community leads</p>
|
|
</section>
|
|
<section class="slide title" data-od-id="slide-6">
|
|
<div class="eyebrow" style="color:#c96442;">Decision needed by July 23 board meeting</div>
|
|
<h1>Approve BYOK-default plus the enterprise self-host license.</h1>
|
|
<p class="body">Owners: Maria Chen (CFO) runs the burn-down plan · Alex Osei (Head of Platform) ships BYOK-default in Q3 · Community leads keep plugin governance fully open throughout.</p>
|
|
</section>
|
|
|
|
<div class="counter" id="counter">1 / 6</div>
|
|
<div class="hint">← / → to navigate</div>
|
|
|
|
<script>
|
|
const slides = document.querySelectorAll('.slide');
|
|
const counter = document.getElementById('counter');
|
|
let active = 0;
|
|
|
|
// Detect the real scroller — when body has `display: flex` + `overflow-x: auto`
|
|
// the scroller can be body OR documentElement depending on the host (in
|
|
// particular, the OD srcdoc iframe). Pick whichever actually overflows.
|
|
function scrollContainers() {
|
|
return [document.scrollingElement, document.documentElement, document.body]
|
|
.filter(Boolean)
|
|
.filter((el, idx, arr) => arr.indexOf(el) === idx);
|
|
}
|
|
function overflowX(el) {
|
|
if (!el) return 0;
|
|
return Math.max(0, (el.scrollWidth || 0) - (el.clientWidth || 0));
|
|
}
|
|
function scrollLeftOf(el) {
|
|
if (!el) return 0;
|
|
try { return Number(el.scrollLeft) || 0; } catch (_) { return 0; }
|
|
}
|
|
function activeScrollLeft() {
|
|
return scrollContainers().reduce((best, el) => {
|
|
const left = scrollLeftOf(el);
|
|
return Math.abs(left) > Math.abs(best) ? left : best;
|
|
}, 0);
|
|
}
|
|
function scroller() {
|
|
return scrollContainers().reduce((best, el) => {
|
|
const bestScore = overflowX(best) + Math.abs(scrollLeftOf(best)) * 2;
|
|
const score = overflowX(el) + Math.abs(scrollLeftOf(el)) * 2;
|
|
return score > bestScore ? el : best;
|
|
}, scrollContainers()[0] || document.documentElement);
|
|
}
|
|
function scrollToLeft(left, behavior) {
|
|
for (const el of scrollContainers()) {
|
|
try { el.scrollLeft = left; } catch (_) {}
|
|
try { el.scrollTo({ left, behavior: behavior || 'smooth' }); } catch (_) {}
|
|
}
|
|
}
|
|
|
|
function go(i) {
|
|
const next = Math.max(0, Math.min(slides.length - 1, i));
|
|
active = next;
|
|
counter.textContent = (next + 1) + ' / ' + slides.length;
|
|
scrollToLeft(next * window.innerWidth, 'smooth');
|
|
}
|
|
function syncFromScroll() {
|
|
const i = Math.round(activeScrollLeft() / window.innerWidth);
|
|
if (i !== active && i >= 0 && i < slides.length) {
|
|
active = i;
|
|
counter.textContent = (i + 1) + ' / ' + slides.length;
|
|
}
|
|
}
|
|
function onKey(e) {
|
|
if (e.defaultPrevented) return;
|
|
if (e.target && (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA')) return;
|
|
if (e.key === 'ArrowRight' || e.key === ' ' || e.key === 'PageDown') { e.preventDefault(); go(active + 1); }
|
|
else if (e.key === 'ArrowLeft' || e.key === 'PageUp') { e.preventDefault(); go(active - 1); }
|
|
else if (e.key === 'Home') { e.preventDefault(); go(0); }
|
|
else if (e.key === 'End') { e.preventDefault(); go(slides.length - 1); }
|
|
}
|
|
// Listen on both window and document in capture phase so the handler
|
|
// fires regardless of which element holds focus inside the iframe.
|
|
window.addEventListener('keydown', onKey, true);
|
|
document.addEventListener('keydown', onKey, true);
|
|
// And listen for scroll on both surfaces — same reason.
|
|
document.addEventListener('scroll', syncFromScroll, { passive: true, capture: true });
|
|
window.addEventListener('scroll', syncFromScroll, { passive: true });
|
|
|
|
// Auto-focus body so arrow keys work without a click.
|
|
document.body.setAttribute('tabindex', '-1');
|
|
document.body.style.outline = 'none';
|
|
function focusDeck() { try { window.focus(); document.body.focus({ preventScroll: true }); } catch (_) {} }
|
|
document.addEventListener('mousedown', focusDeck);
|
|
window.addEventListener('load', focusDeck);
|
|
focusDeck();
|
|
</script>
|
|
|
|
</body>
|
|
</html>
|