first-commit
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
ci / Validate workspace (push) Has been cancelled
landing-page-ci / Validate landing page (push) Has been cancelled
landing-page-deploy / Deploy landing page (push) Has been cancelled
github-metrics / Generate repository metrics SVG (push) Has been cancelled
refresh-contributors-wall / Refresh contributors wall cache bust (push) Waiting to run
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
# Shared device frames
|
||||
|
||||
Reusable, pixel-accurate device chrome that any skill can compose into a
|
||||
multi-device or multi-screen layout. Each frame is a self-contained HTML
|
||||
snippet that renders a device shell and embeds its inner screen via an
|
||||
\`<iframe src="?screen=...">\` query parameter.
|
||||
|
||||
## Why these exist
|
||||
|
||||
The mobile-app skill has a one-screen iPhone frame baked into its seed
|
||||
template. That covers ~80% of mobile prototypes. These shared frames cover
|
||||
the remaining 20%:
|
||||
|
||||
- **Multi-screen flows** — three iPhones side by side showing onboarding 1
|
||||
/ 2 / 3.
|
||||
- **Multi-device sets** — desktop + tablet + phone of the same product.
|
||||
- **Future skills** — \`watch-app\`, \`tablet-app\`, \`tv-app\` can reuse
|
||||
these without re-inventing the chrome.
|
||||
|
||||
## Files
|
||||
|
||||
\`\`\`
|
||||
assets/frames/
|
||||
├── README.md ← you're reading this
|
||||
├── iphone-15-pro.html ← 390×844 + Dynamic Island
|
||||
├── android-pixel.html ← 412×900 + punch-hole camera
|
||||
├── ipad-pro.html ← 1024×1366 + USB-C edge
|
||||
├── macbook.html ← 1440×900 inside laptop chrome
|
||||
└── browser-chrome.html ← Safari/Chrome window with traffic lights
|
||||
\`\`\`
|
||||
|
||||
## Usage
|
||||
|
||||
Each frame accepts a \`?screen=<path>\` query parameter and renders that
|
||||
path inside its inner viewport:
|
||||
|
||||
\`\`\`html
|
||||
<iframe
|
||||
src="../../assets/frames/iphone-15-pro.html?screen=screens/home.html"
|
||||
width="390"
|
||||
height="844"
|
||||
loading="lazy"
|
||||
></iframe>
|
||||
\`\`\`
|
||||
|
||||
In an OD-managed project, the recommended pattern is:
|
||||
|
||||
\`\`\`
|
||||
my-project/
|
||||
├── index.html ← gallery view: composes 3+ frames in a row
|
||||
├── screens/
|
||||
│ ├── home.html ← inner content rendered inside iphone-15-pro.html
|
||||
│ ├── search.html
|
||||
│ └── detail.html
|
||||
└── (no copy of frames — point at the shared assets folder)
|
||||
\`\`\`
|
||||
|
||||
## Design tokens
|
||||
|
||||
Each frame reads its inner screen's tokens via \`postMessage\` if you want
|
||||
the bezel to tint with the active palette. The default state is "phone in
|
||||
hand" — neutral metallic — which works against any background.
|
||||
|
||||
## Authoring rules
|
||||
|
||||
When extending this library:
|
||||
|
||||
1. **No external assets.** Inline all SVG. No font imports. No image URLs.
|
||||
2. **One frame per file.** Don't bundle iPhone + Android in one HTML.
|
||||
3. **\`?screen=\` query is the only contract.** Don't introduce other
|
||||
query params; the harness has to be predictable for skills to use.
|
||||
4. **The frame is decorative chrome only.** All content lives in the inner
|
||||
screen file. The frame must work with `?screen=about:blank` (showing
|
||||
just the device shell).
|
||||
5. **Match real device dimensions.** iPhone 15 Pro is 390×844 logical
|
||||
pixels. iPad Pro 11" is 834×1194. Don't ship a "looks like" frame —
|
||||
the seed has to match.
|
||||
@@ -0,0 +1,158 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
Shared frame: Pixel 8 Pro (412 × 900).
|
||||
Usage: <iframe src="android-pixel.html?screen=path/to/screen.html"></iframe>
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Pixel 8 Pro frame</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: transparent; }
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font: 14px/1.4 'Roboto', -apple-system, BlinkMacSystemFont, system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.device {
|
||||
position: relative;
|
||||
width: 412px;
|
||||
height: 900px;
|
||||
border-radius: 44px;
|
||||
padding: 10px;
|
||||
background:
|
||||
linear-gradient(160deg, #1a1a1a 0%, #0a0a0a 100%);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.05) inset,
|
||||
0 0 0 2px #000 inset,
|
||||
0 28px 60px -12px rgba(0,0,0,0.45),
|
||||
0 8px 20px -8px rgba(0,0,0,0.35);
|
||||
}
|
||||
/* power button (right) */
|
||||
.device::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: 160px;
|
||||
width: 4px; height: 70px;
|
||||
background: #0a0a0a;
|
||||
border-radius: 2px;
|
||||
}
|
||||
/* volume rocker (right, below power) */
|
||||
.device::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -3px;
|
||||
top: 240px;
|
||||
width: 4px; height: 110px;
|
||||
background: #0a0a0a;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
/* punch-hole camera, top-center */
|
||||
.punch-hole {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 12px;
|
||||
height: 12px;
|
||||
background: #000;
|
||||
border-radius: 50%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.screen {
|
||||
position: relative;
|
||||
width: 100%; height: 100%;
|
||||
background: #fafaf7;
|
||||
border-radius: 36px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.statusbar {
|
||||
flex: 0 0 36px;
|
||||
padding: 12px 20px 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
color: var(--statusbar-fg, #1a1916);
|
||||
pointer-events: none;
|
||||
}
|
||||
.statusbar .right { display: inline-flex; align-items: center; gap: 5px; }
|
||||
.statusbar svg { width: 14px; height: 10px; fill: currentColor; }
|
||||
.statusbar .battery { width: 22px; }
|
||||
|
||||
.inner {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: #fafaf7;
|
||||
}
|
||||
|
||||
/* Android nav bar — three-button (back / home / recents) */
|
||||
.navbar {
|
||||
flex: 0 0 28px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
pointer-events: none;
|
||||
color: var(--navbar-fg, #1a1916);
|
||||
opacity: 0.7;
|
||||
}
|
||||
.navbar svg { width: 14px; height: 14px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="device">
|
||||
<span class="punch-hole" aria-hidden></span>
|
||||
|
||||
<div class="screen">
|
||||
<div class="statusbar">
|
||||
<span>9:41</span>
|
||||
<span class="right">
|
||||
<svg viewBox="0 0 14 10" aria-hidden><path d="M0 9h2V5H0v4zm4 0h2V3H4v6zm4 0h2V1H8v8zm4 0h2V0h-2v9z"/></svg>
|
||||
<svg viewBox="0 0 14 10" aria-hidden><path d="M7 1.2c-2.4 0-4.6.9-6.4 2.5L2 5.2c1.4-1.2 3.1-1.9 5-1.9s3.6.7 5 1.9l1.4-1.5C11.6 2.1 9.4 1.2 7 1.2zm0 3c-1.5 0-2.9.6-4 1.5L4.4 7.2c.7-.7 1.7-1 2.6-1s1.9.3 2.6 1L11 5.7c-1.1-.9-2.5-1.5-4-1.5zm0 3c-.7 0-1.4.3-1.9.8l1.9 1.9 1.9-1.9c-.5-.5-1.2-.8-1.9-.8z"/></svg>
|
||||
<svg class="battery" viewBox="0 0 22 10" aria-hidden>
|
||||
<rect x="0.5" y="0.5" width="18" height="9" rx="2" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
|
||||
<rect x="19" y="3" width="1.5" height="4" rx="0.4" fill="currentColor" fill-opacity="0.45"/>
|
||||
<rect x="2" y="2" width="15" height="6" rx="1"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
class="inner"
|
||||
id="screen"
|
||||
title="Inner screen"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
loading="lazy"
|
||||
src="about:blank"
|
||||
></iframe>
|
||||
|
||||
<nav class="navbar" aria-hidden>
|
||||
<svg viewBox="0 0 14 14"><path d="M9 2 4 7l5 5" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
|
||||
<svg viewBox="0 0 14 14"><circle cx="7" cy="7" r="5" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
|
||||
<svg viewBox="0 0 14 14"><rect x="2" y="2" width="10" height="10" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.6"/></svg>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var src = qs.get('screen');
|
||||
var iframe = document.getElementById('screen');
|
||||
if (src) iframe.src = src;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,130 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
Shared frame: macOS Safari-style browser window (traffic lights + URL bar).
|
||||
Usage: <iframe src="browser-chrome.html?screen=path/to/page.html&url=example.com"></iframe>
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>Browser frame</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: transparent; }
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font: 13px/1.4 -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
|
||||
color: #1a1916;
|
||||
}
|
||||
|
||||
.window {
|
||||
position: relative;
|
||||
width: min(960px, 96vw);
|
||||
aspect-ratio: 16 / 10;
|
||||
background: #fafaf7;
|
||||
border-radius: 12px;
|
||||
overflow: hidden;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(0,0,0,0.06) inset,
|
||||
0 1px 0 rgba(255,255,255,0.95) inset,
|
||||
0 28px 60px -12px rgba(0,0,0,0.18),
|
||||
0 8px 20px -8px rgba(0,0,0,0.12);
|
||||
}
|
||||
|
||||
.titlebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
padding: 10px 14px;
|
||||
background: linear-gradient(to bottom, #ececeb 0%, #dadad8 100%);
|
||||
border-bottom: 1px solid rgba(0,0,0,0.08);
|
||||
height: 38px;
|
||||
}
|
||||
|
||||
.lights {
|
||||
display: inline-flex;
|
||||
gap: 8px;
|
||||
}
|
||||
.light {
|
||||
width: 12px; height: 12px;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 0.5px rgba(0,0,0,0.15) inset;
|
||||
}
|
||||
.light.r { background: #ff5f57; }
|
||||
.light.y { background: #febc2e; }
|
||||
.light.g { background: #28c840; }
|
||||
|
||||
.url {
|
||||
flex: 1;
|
||||
max-width: 60%;
|
||||
margin: 0 auto;
|
||||
background: #fff;
|
||||
border: 1px solid rgba(0,0,0,0.08);
|
||||
border-radius: 6px;
|
||||
padding: 4px 12px;
|
||||
font-size: 12px;
|
||||
color: #6b6964;
|
||||
text-align: center;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.url .lock { color: #6b6964; margin-right: 6px; }
|
||||
|
||||
.tabs {
|
||||
display: inline-flex;
|
||||
gap: 6px;
|
||||
align-items: center;
|
||||
color: #6b6964;
|
||||
}
|
||||
.tabs svg { width: 14px; height: 14px; stroke: currentColor; fill: none; stroke-width: 1.6; }
|
||||
|
||||
.inner {
|
||||
width: 100%;
|
||||
height: calc(100% - 38px);
|
||||
border: 0;
|
||||
background: #fafaf7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="window">
|
||||
<div class="titlebar">
|
||||
<span class="lights">
|
||||
<span class="light r"></span>
|
||||
<span class="light y"></span>
|
||||
<span class="light g"></span>
|
||||
</span>
|
||||
<span class="tabs">
|
||||
<svg viewBox="0 0 14 14"><path d="M9 3 4 7l5 4"/></svg>
|
||||
<svg viewBox="0 0 14 14"><path d="M5 3l5 4-5 4"/></svg>
|
||||
</span>
|
||||
<div class="url" id="url"><span class="lock">🔒</span><span id="url-text">example.com</span></div>
|
||||
<span class="tabs">
|
||||
<svg viewBox="0 0 14 14"><path d="M2 7h10M7 2v10"/></svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
class="inner"
|
||||
id="screen"
|
||||
title="Inner page"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
loading="lazy"
|
||||
src="about:blank"
|
||||
></iframe>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var src = qs.get('screen');
|
||||
var url = qs.get('url');
|
||||
var iframe = document.getElementById('screen');
|
||||
if (src) iframe.src = src;
|
||||
if (url) document.getElementById('url-text').textContent = url;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,96 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
Shared frame: iPad Pro 11" (834 × 1194 logical, displayed at 70% scale).
|
||||
Usage: <iframe src="ipad-pro.html?screen=path/to/screen.html"></iframe>
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>iPad Pro frame</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: transparent; }
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font: 15px/1.4 -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.device {
|
||||
position: relative;
|
||||
width: 584px;
|
||||
height: 836px;
|
||||
border-radius: 36px;
|
||||
padding: 14px;
|
||||
background:
|
||||
linear-gradient(160deg, #2a2a2c 0%, #1a1a1c 50%, #0e0e10 100%);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.04) inset,
|
||||
0 0 0 2px #000 inset,
|
||||
0 28px 60px -12px rgba(0,0,0,0.45),
|
||||
0 8px 20px -8px rgba(0,0,0,0.35);
|
||||
}
|
||||
/* power + volume on right edge */
|
||||
.device::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
right: -3px; top: 80px;
|
||||
width: 4px; height: 56px;
|
||||
background: #0a0a0c; border-radius: 2px;
|
||||
}
|
||||
|
||||
/* front camera, top-center landscape position (we render portrait here) */
|
||||
.camera {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
background: #0a0a0c;
|
||||
border-radius: 50%;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.screen {
|
||||
position: relative;
|
||||
width: 100%; height: 100%;
|
||||
background: #fafaf7;
|
||||
border-radius: 22px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
.inner {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: #fafaf7;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="device">
|
||||
<span class="camera" aria-hidden></span>
|
||||
<div class="screen">
|
||||
<iframe
|
||||
class="inner"
|
||||
id="screen"
|
||||
title="Inner screen"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
loading="lazy"
|
||||
src="about:blank"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<script>
|
||||
(function () {
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var src = qs.get('screen');
|
||||
var iframe = document.getElementById('screen');
|
||||
if (src) iframe.src = src;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,175 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
Shared frame: iPhone 15 Pro (390 × 844).
|
||||
Usage: <iframe src="iphone-15-pro.html?screen=path/to/screen.html"></iframe>
|
||||
Renders: the bezel + Dynamic Island + status bar + home indicator,
|
||||
with the ?screen content embedded as an iframe in the screen area.
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>iPhone 15 Pro frame</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: transparent; }
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font: 15px/1.4 -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
|
||||
color: #1a1916;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
.device {
|
||||
position: relative;
|
||||
width: 390px;
|
||||
height: 844px;
|
||||
border-radius: 56px;
|
||||
padding: 12px;
|
||||
background:
|
||||
linear-gradient(160deg, #2a2a2c 0%, #1a1a1c 50%, #0e0e10 100%);
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.04) inset,
|
||||
0 0 0 2px #000 inset,
|
||||
0 28px 60px -12px rgba(0,0,0,0.45),
|
||||
0 8px 20px -8px rgba(0,0,0,0.35);
|
||||
isolation: isolate;
|
||||
}
|
||||
.device::before, .device::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
width: 3px;
|
||||
background: linear-gradient(to bottom, transparent 0%, rgba(255,255,255,0.06) 8%, transparent 16%, transparent 84%, rgba(255,255,255,0.04) 92%, transparent 100%);
|
||||
top: 100px;
|
||||
bottom: 100px;
|
||||
pointer-events: none;
|
||||
}
|
||||
.device::before { left: -1px; }
|
||||
.device::after { right: -1px; }
|
||||
|
||||
.island {
|
||||
position: absolute;
|
||||
top: 22px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 124px;
|
||||
height: 36px;
|
||||
background: #000;
|
||||
border-radius: 999px;
|
||||
z-index: 5;
|
||||
}
|
||||
|
||||
.btn-rail {
|
||||
position: absolute;
|
||||
width: 4px;
|
||||
background: #0a0a0c;
|
||||
border-radius: 2px;
|
||||
}
|
||||
.btn-rail.left-1 { left: -3px; top: 174px; height: 32px; }
|
||||
.btn-rail.left-2 { left: -3px; top: 220px; height: 60px; }
|
||||
.btn-rail.left-3 { left: -3px; top: 290px; height: 60px; }
|
||||
.btn-rail.right-1 { right: -3px; top: 250px; height: 100px; }
|
||||
|
||||
.screen {
|
||||
position: relative;
|
||||
width: 100%; height: 100%;
|
||||
background: #fafaf7;
|
||||
border-radius: 44px;
|
||||
overflow: hidden;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.statusbar {
|
||||
flex: 0 0 47px;
|
||||
padding: 18px 26px 0;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
justify-content: space-between;
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
letter-spacing: -0.01em;
|
||||
color: var(--statusbar-fg, #1a1916);
|
||||
pointer-events: none;
|
||||
}
|
||||
.statusbar .right { display: inline-flex; align-items: center; gap: 6px; }
|
||||
.statusbar svg { width: 17px; height: 11px; fill: currentColor; }
|
||||
.statusbar .battery { width: 25px; }
|
||||
|
||||
.inner {
|
||||
flex: 1 1 auto;
|
||||
width: 100%;
|
||||
border: 0;
|
||||
background: #fafaf7;
|
||||
}
|
||||
|
||||
.home-indicator {
|
||||
flex: 0 0 28px;
|
||||
position: relative;
|
||||
pointer-events: none;
|
||||
}
|
||||
.home-indicator::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 50%; bottom: 8px;
|
||||
transform: translateX(-50%);
|
||||
width: 134px; height: 5px;
|
||||
background: var(--home-fg, #1a1916);
|
||||
border-radius: 999px;
|
||||
opacity: 0.85;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="device">
|
||||
<span class="btn-rail left-1" aria-hidden></span>
|
||||
<span class="btn-rail left-2" aria-hidden></span>
|
||||
<span class="btn-rail left-3" aria-hidden></span>
|
||||
<span class="btn-rail right-1" aria-hidden></span>
|
||||
<span class="island" aria-hidden></span>
|
||||
|
||||
<div class="screen">
|
||||
<div class="statusbar">
|
||||
<span>9:41</span>
|
||||
<span class="right">
|
||||
<svg viewBox="0 0 17 11" aria-hidden>
|
||||
<rect x="0" y="7" width="3" height="4" rx="0.6"/>
|
||||
<rect x="4" y="5" width="3" height="6" rx="0.6"/>
|
||||
<rect x="8" y="3" width="3" height="8" rx="0.6"/>
|
||||
<rect x="12" y="0" width="3" height="11" rx="0.6"/>
|
||||
</svg>
|
||||
<svg viewBox="0 0 17 11" aria-hidden>
|
||||
<path d="M8.5 1.5C5.5 1.5 2.7 2.6 0.5 4.6L2 6.1C3.8 4.5 6.1 3.6 8.5 3.6c2.4 0 4.7 0.9 6.5 2.5l1.5-1.5c-2.2-2-5-3.1-8-3.1zM3.5 7.6L5 9.1c1-0.9 2.2-1.4 3.5-1.4 1.3 0 2.5 0.5 3.5 1.4l1.5-1.5c-1.4-1.3-3.1-2-5-2-1.9 0-3.6 0.7-5 2zM6.5 10.6l2 2 2-2c-0.5-0.5-1.2-0.8-2-0.8s-1.5 0.3-2 0.8z"/>
|
||||
</svg>
|
||||
<svg class="battery" viewBox="0 0 25 11" aria-hidden>
|
||||
<rect x="0.5" y="0.5" width="21" height="10" rx="2.5" fill="none" stroke="currentColor" stroke-opacity="0.45"/>
|
||||
<rect x="22" y="3.5" width="1.5" height="4" rx="0.4" fill="currentColor" fill-opacity="0.45"/>
|
||||
<rect x="2" y="2" width="18" height="7" rx="1.4"/>
|
||||
</svg>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<iframe
|
||||
class="inner"
|
||||
id="screen"
|
||||
title="Inner screen"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
loading="lazy"
|
||||
src="about:blank"
|
||||
></iframe>
|
||||
|
||||
<div class="home-indicator" aria-hidden></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var src = qs.get('screen');
|
||||
var iframe = document.getElementById('screen');
|
||||
if (src) iframe.src = src;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,135 @@
|
||||
<!doctype html>
|
||||
<!--
|
||||
Shared frame: MacBook Pro 14" with display + lid + bottom keyboard hint.
|
||||
The lid renders a 1440×900 viewport scaled to 720×450 (50%) so the
|
||||
whole laptop fits in a typical 880×600 layout cell. Pass the actual
|
||||
desktop screen via ?screen= and it renders at full 1440×900 inside.
|
||||
-->
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<title>MacBook frame</title>
|
||||
<style>
|
||||
*, *::before, *::after { box-sizing: border-box; }
|
||||
html, body { margin: 0; padding: 0; height: 100%; background: transparent; }
|
||||
body {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font: 14px/1.4 -apple-system, BlinkMacSystemFont, 'SF Pro Text', system-ui, sans-serif;
|
||||
}
|
||||
|
||||
.laptop {
|
||||
position: relative;
|
||||
width: 880px;
|
||||
}
|
||||
|
||||
/* lid */
|
||||
.lid {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
aspect-ratio: 1440 / 900;
|
||||
background: #1a1a1a;
|
||||
border-radius: 18px 18px 4px 4px;
|
||||
padding: 14px 14px 18px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.05) inset,
|
||||
0 8px 20px -8px rgba(0,0,0,0.4);
|
||||
}
|
||||
|
||||
/* notch */
|
||||
.lid::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 156px;
|
||||
height: 18px;
|
||||
background: #1a1a1a;
|
||||
border-radius: 0 0 12px 12px;
|
||||
z-index: 5;
|
||||
}
|
||||
/* camera dot */
|
||||
.lid::after {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 5px; height: 5px;
|
||||
background: #0a0a0a;
|
||||
border-radius: 50%;
|
||||
box-shadow: 0 0 0 1px rgba(255,255,255,0.04);
|
||||
z-index: 6;
|
||||
}
|
||||
|
||||
.display {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background: #fafaf7;
|
||||
border-radius: 4px;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
.display iframe {
|
||||
width: 1440px;
|
||||
height: 900px;
|
||||
border: 0;
|
||||
background: #fafaf7;
|
||||
transform: scale(calc(852 / 1440)); /* 880 - 28 padding = 852 inner */
|
||||
transform-origin: top left;
|
||||
}
|
||||
|
||||
/* base / chin */
|
||||
.base {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 14px;
|
||||
margin-top: -1px;
|
||||
background:
|
||||
linear-gradient(to bottom, #c0c0c4 0%, #9a9a9e 70%, #7a7a7e 100%);
|
||||
border-radius: 0 0 6px 6px;
|
||||
box-shadow:
|
||||
0 0 0 1px rgba(255,255,255,0.4) inset,
|
||||
0 6px 12px -4px rgba(0,0,0,0.25);
|
||||
}
|
||||
.base::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
width: 96px;
|
||||
height: 4px;
|
||||
background: rgba(0,0,0,0.18);
|
||||
border-radius: 0 0 6px 6px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="laptop">
|
||||
<div class="lid">
|
||||
<div class="display">
|
||||
<iframe
|
||||
id="screen"
|
||||
title="Inner screen"
|
||||
sandbox="allow-scripts allow-same-origin"
|
||||
loading="lazy"
|
||||
src="about:blank"
|
||||
></iframe>
|
||||
</div>
|
||||
</div>
|
||||
<div class="base" aria-hidden></div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
(function () {
|
||||
var qs = new URLSearchParams(location.search);
|
||||
var src = qs.get('screen');
|
||||
var iframe = document.getElementById('screen');
|
||||
if (src) iframe.src = src;
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user