Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 18 additions & 5 deletions canvas/src/canvasLibrary.test.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,27 @@
import { describe, expect, it } from 'vitest'
import { canvasFileHtml, loadCanvasFileHtml, readCanvasLibrary } from './canvasLibrary'
import {
canvasFileHtml,
loadCanvasFileHtml,
readCanvasLayout,
readCanvasLibrary,
} from './canvasLibrary'
import { WELCOME_PAGE_SLUG } from './canvasUrl'

describe('readCanvasLibrary', () => {
it('puts the welcome page first, then `order`, then slug order', () => {
const slugs = readCanvasLibrary().map((files) => files[0].pageSlug)
// snapaction-ios is the one folder that declares an order (-1).
expect(slugs.slice(0, 2)).toEqual([WELCOME_PAGE_SLUG, 'snapaction-ios'])
const rest = slugs.slice(2)
expect(rest).toEqual([...rest].sort((a, b) => a.localeCompare(b, undefined, { numeric: true })))
expect(slugs[0]).toBe(WELCOME_PAGE_SLUG)
// Three folders declare an order: snapaction-ios at -1 ahead of the alphabet,
// apple-icons at 1 and templates at 2 behind it. Sorting the rest by the same
// rule rather than naming them keeps this passing when a fourth one does.
const rest = slugs.slice(1)
const order = (slug: string) => readCanvasLayout(slug)?.order ?? 0
expect(rest).toEqual(
[...rest].sort(
(a, b) => order(a) - order(b) || a.localeCompare(b, undefined, { numeric: true }),
),
)
expect(rest.slice(-2)).toEqual(['apple-icons', 'templates'])
})
})

Expand Down
15 changes: 15 additions & 0 deletions mockups/canvases/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,21 @@ declares a box other than the default 478 x 980.
or two thirds of a point, were worth more than the blur fit and the compose
glyph together. The seven `ref-*` boards are gitignored, so a fresh clone
has 10.
- `apple-app-store/`: the run to read when the source is native iPhone 16 Pro
screenshots rather than Mobbin or Figma. Nine screens of the iOS 26 App
Store - five tab roots, three sheets and a system alert - in 23 boards
across three rows: a token board, a type board and three evidence boards for
80 tokens, the nine replicas at 402 x 874, and the capture of each
underneath. Mean absolute delta is 3.62-8.02 levels (of 255). Its
`README.md` records the rule that splits the pixels. Interface is rebuilt,
glyphs traced to SVG. Every app icon is the 1024 px original from the iTunes
lookup API. Only editorial pictures are crops, and the type, icons and
pills set over them are inpainted out of the crop and drawn again live.
Rebuilding took the range from 2.32-8.00 and is kept on purpose. It also
records the status bar, copied from `templates/` byte for byte, with none
of the captures' own clocks, badges or iOS 26 glyphs carried over. And it
records p2, where two stacked 20% scrims land exactly on 255 x .8 x .8. The nine `ref-*` boards are gitignored, so a
fresh clone has 14.
- `templates/`: the starting point, not a finished board. The four boards
every run produces (design tokens, evidence, one phone screen, one parked
reference) with placeholder values, generated from one list of tokens so
Expand Down
176 changes: 176 additions & 0 deletions mockups/canvases/apple-app-store/00-design-tokens.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>App Store - Design Tokens</title>
<style>
:root{
/* Font */
--as-font:-apple-system,BlinkMacSystemFont,"SF Pro Text","SF Pro Display","Helvetica Neue",Helvetica,Arial,sans-serif;

/* Surface */
--as-bg:#FFFFFF;
--as-fill:#EEEEEF;
--as-fill-2:#EBEBEB;
--as-chip:#FFFFFF;
--as-circle:#F9F9FA;
--as-tint:#EDF6FF;
--as-badge:#86C0FD;
--as-sky:linear-gradient(#023254,#034B72 41%,#04608A 72%,#056792 85%,#1A719E);
--as-scrim:rgba(0,0,0,.20);
--as-group:#F2F2F6;
--as-haze:#F9FAFA;
--as-disc:#FAFAFB;
--as-disc-2:#F5F5F9;
--as-back:#E0E0E0;
--as-alert:rgba(255,255,255,.60);
--as-alert-btn:rgba(0,0,0,.12);
--as-kbd:#E2E3E7;
--as-glass-pill:rgba(255,255,255,.41);
--as-glass-price:rgba(255,255,255,.12);

/* Line */
--as-hairline:#DFDFDF;
--as-border:#B0AFB0;
--as-sep:#E7E7E8;
--as-kbd-sep:#D3D4DA;

/* Ink */
--as-ink:#000000;
--as-ink-2:#8A8A8E;
--as-ink-3:#BFBFBF;
--as-ink-inv:#FFFFFF;
--as-ink-dim:#707172;
--as-ink-note:#85858B;
--as-ink-ph:#BCBCC1;
--as-ink-chev:#C5C5C7;
--as-ink-alert:#838383;
--as-ink-kbd:#494A4B;
--as-ink-hint:#CDCED0;
--as-ink-off:#E5E6E9;
--as-ink-glass:rgba(255,255,255,.70);
--as-ink-glass-2:rgba(255,255,255,.55);
--as-ink-promo:#FFFF7D;

/* Accent */
--as-accent:#0082F7;
--as-accent-2:#0088FE;
--as-accent-3:#007AFF;

/* Radius */
--as-r-card:26px;
--as-r-sheet:38px;
--as-r-alert:34px;
--as-r-chip:18.5px;
--as-r-pill:999px;
--as-r-key:6px;
--as-r-kbd:28px;
--as-r-phone:62px;

/* Type */
--as-t-title:700 34px/41px var(--as-font);
--as-t-section:700 21px/26px var(--as-font);
--as-t-group:700 16px/20px var(--as-font);
--as-t-row:400 16px/18px var(--as-font);
--as-t-sub:400 12px/21px var(--as-font);
--as-t-chip:600 14px/19px var(--as-font);
--as-tr-tight:-0.2px;
--as-t-cta:600 15px/20px var(--as-font);
--as-t-iap:400 8px/11px var(--as-font);
--as-t-lock:600 14px/18px var(--as-font);
--as-t-lock-sub:400 11px/14px var(--as-font);
--as-t-price:700 15px/20px var(--as-font);
--as-t-tile:600 16px/19.33px var(--as-font);
--as-t-wordmark:500 20px/24px var(--as-font);
--as-tr-wordmark:-0.05px;
--as-t-head:600 21px/26px var(--as-font);
--as-t-headsub:400 22px/26px var(--as-font);
--as-t-eyebrow:700 11px/16px var(--as-font);
--as-tr-head:-0.5px;
--as-t-ssub:400 14px/18px var(--as-font);
--as-t-ph:400 16px/20px var(--as-font);
--as-t-ad:600 10px/13px var(--as-font);
--as-t-tab:500 10px/12px var(--as-font);
--as-t-hero:700 26px/32px var(--as-font);
--as-t-btn:600 16.5px/22px var(--as-font);
--as-t-foot:400 12px/16px var(--as-font);
--as-t-areye:600 12px/16px var(--as-font);
--as-t-pane:700 27px/32px var(--as-font);
--as-t-lede:400 21px/26px var(--as-font);
--as-t-go:600 16px/22px var(--as-font);
--as-t-link:600 16px/22px var(--as-font);
--as-t-body:400 16px/22px var(--as-font);
--as-t-sheet:600 17px/22px var(--as-font);
--as-t-note:400 12px/14px var(--as-font);
--as-t-signin:700 21px/28px var(--as-font);
--as-t-alert:600 17px/22px var(--as-font);
--as-t-alert-msg:400 15px/20px var(--as-font);
--as-t-alert-btn:500 17px/22px var(--as-font);
--as-t-key:400 25px/35px var(--as-font);
--as-t-key-sm:400 18px/40px var(--as-font);
--as-t-key-at:400 21px/38px var(--as-font);
--as-t-pw:400 17px/22px var(--as-font);
--as-t-time:590 17px/22px var(--as-font);

/* Metrics */
--as-w:402px;
--as-h:874px;
--as-status:62px;
--as-gutter:20px;
--as-icon:62px;
--as-bar-w:361px;
--as-bar-h:62.7px;
--as-bar-top:790.5px;
--as-tab-pitch:68.7px;
}

*{box-sizing:border-box;margin:0;padding:0}
body{font-family:var(--as-font);-webkit-font-smoothing:antialiased;
display:flex;justify-content:center;padding:24px}
.phone{position:relative;flex:none;width:var(--as-w);height:var(--as-h);
border-radius:var(--as-r-phone);overflow:hidden;background:var(--as-bg);color:var(--as-ink);
transform:translateZ(0);
box-shadow:0 0 0 11px #1D191A,0 0 0 12.5px #3A3735,0 24px 60px rgba(29,25,26,.28)}
.sb{position:absolute;left:0;top:0;width:var(--as-w);height:var(--as-status);z-index:6}
.sb .time{position:absolute;left:0;top:18.2px;width:142.4px;text-align:center;font:var(--as-t-time)}
.sb .island{position:absolute;top:11px;left:50%;transform:translateX(-50%);
width:125px;height:36px;border-radius:20px;background:#000}
.sb svg{position:absolute;display:block;fill:currentColor}
/* the one change for a 402pt frame: the template's glyphs, moved as a group */
.sb .glyphs{position:absolute;left:6px;top:0}
/* iOS picks the indicator colour against the wallpaper: measure it per screen */
.home{position:absolute;left:50%;bottom:8px;transform:translateX(-50%);
width:139px;height:5px;border-radius:3px;background:currentColor;z-index:6}
body{padding:0;background:var(--as-bg);color:var(--as-ink)}
.sheet{width:478px;height:980px;padding:20px;overflow:hidden}
h1{font:600 17px/22px var(--as-font);margin-bottom:2px}
header p{font:var(--as-t-note);color:var(--as-ink-3);margin-bottom:14px}
h2{font:600 9px/12px var(--as-font);letter-spacing:.8px;text-transform:uppercase;
color:var(--as-ink-3);margin:12px 0 5px}
.grid{display:grid;grid-template-columns:repeat(4,1fr);gap:6px}
.sw .chip{height:26px;border-radius:6px;border:1px solid var(--as-border)}
.sw b{display:block;margin-top:3px;font:600 8.5px/11px ui-monospace,Menlo,monospace}
.sw i{display:block;font:400 8px/11px ui-monospace,Menlo,monospace;
color:var(--as-ink-3);font-style:normal;word-break:break-all}
.rad{display:flex;gap:9px;flex-wrap:wrap}
.rb{width:44px;height:26px;background:var(--as-bg);border:1px solid var(--as-border)}
.rad em{display:block;margin-top:2px;font:400 8.5px/11px var(--as-font);
color:var(--as-ink-3);font-style:normal;text-align:center}
.tr{display:flex;align-items:baseline;justify-content:space-between;gap:10px;
padding-bottom:2px;border-bottom:1px solid var(--as-hairline)}
.tr span{white-space:nowrap;overflow:hidden}
.tr em{font:400 8px/11px ui-monospace,Menlo,monospace;color:var(--as-ink-3);
font-style:normal;white-space:nowrap;flex:none}
.met{font:400 9px/13px ui-monospace,Menlo,monospace;color:var(--as-ink-2)}
table.ev{width:100%;border-collapse:collapse}
table.ev td{vertical-align:top;padding:2.5px 6px 2.5px 0;
border-bottom:1px solid var(--as-hairline);font:400 8.5px/11px var(--as-font)}
td.t,td.v{font-family:ui-monospace,Menlo,monospace}
td.t{color:var(--as-accent);white-space:nowrap}
td.v{color:var(--as-ink-2);max-width:120px;word-break:break-all}
td.e{color:var(--as-ink-3)}</style>
</head>
<body>
<div class="sheet"><header><h1>App Store</h1><p>iPhone 16 Pro, 402 &times; 874 pt. Captures are @3x, so SCALE = 3.0 and every number below is a direct read.</p></header><h2>Colour</h2><div class="grid"><div class="sw"><div class="chip" style="background:var(--as-bg)"></div><b>--as-bg</b><i>#FFFFFF</i></div><div class="sw"><div class="chip" style="background:var(--as-fill)"></div><b>--as-fill</b><i>#EEEEEF</i></div><div class="sw"><div class="chip" style="background:var(--as-fill-2)"></div><b>--as-fill-2</b><i>#EBEBEB</i></div><div class="sw"><div class="chip" style="background:var(--as-chip)"></div><b>--as-chip</b><i>#FFFFFF</i></div><div class="sw"><div class="chip" style="background:var(--as-circle)"></div><b>--as-circle</b><i>#F9F9FA</i></div><div class="sw"><div class="chip" style="background:var(--as-tint)"></div><b>--as-tint</b><i>#EDF6FF</i></div><div class="sw"><div class="chip" style="background:var(--as-badge)"></div><b>--as-badge</b><i>#86C0FD</i></div><div class="sw"><div class="chip" style="background:var(--as-sky)"></div><b>--as-sky</b><i>linear-gradient(#023254,#034B72 41%,#04608A 72%,#056792 85%,#1A719E)</i></div><div class="sw"><div class="chip" style="background:var(--as-scrim)"></div><b>--as-scrim</b><i>rgba(0,0,0,.20)</i></div><div class="sw"><div class="chip" style="background:var(--as-group)"></div><b>--as-group</b><i>#F2F2F6</i></div><div class="sw"><div class="chip" style="background:var(--as-haze)"></div><b>--as-haze</b><i>#F9FAFA</i></div><div class="sw"><div class="chip" style="background:var(--as-disc)"></div><b>--as-disc</b><i>#FAFAFB</i></div><div class="sw"><div class="chip" style="background:var(--as-disc-2)"></div><b>--as-disc-2</b><i>#F5F5F9</i></div><div class="sw"><div class="chip" style="background:var(--as-back)"></div><b>--as-back</b><i>#E0E0E0</i></div><div class="sw"><div class="chip" style="background:var(--as-alert)"></div><b>--as-alert</b><i>rgba(255,255,255,.60)</i></div><div class="sw"><div class="chip" style="background:var(--as-alert-btn)"></div><b>--as-alert-btn</b><i>rgba(0,0,0,.12)</i></div><div class="sw"><div class="chip" style="background:var(--as-kbd)"></div><b>--as-kbd</b><i>#E2E3E7</i></div><div class="sw"><div class="chip" style="background:var(--as-glass-pill)"></div><b>--as-glass-pill</b><i>rgba(255,255,255,.41)</i></div><div class="sw"><div class="chip" style="background:var(--as-glass-price)"></div><b>--as-glass-price</b><i>rgba(255,255,255,.12)</i></div><div class="sw"><div class="chip" style="background:var(--as-hairline)"></div><b>--as-hairline</b><i>#DFDFDF</i></div><div class="sw"><div class="chip" style="background:var(--as-border)"></div><b>--as-border</b><i>#B0AFB0</i></div><div class="sw"><div class="chip" style="background:var(--as-sep)"></div><b>--as-sep</b><i>#E7E7E8</i></div><div class="sw"><div class="chip" style="background:var(--as-kbd-sep)"></div><b>--as-kbd-sep</b><i>#D3D4DA</i></div><div class="sw"><div class="chip" style="background:var(--as-ink)"></div><b>--as-ink</b><i>#000000</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-2)"></div><b>--as-ink-2</b><i>#8A8A8E</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-3)"></div><b>--as-ink-3</b><i>#BFBFBF</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-inv)"></div><b>--as-ink-inv</b><i>#FFFFFF</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-dim)"></div><b>--as-ink-dim</b><i>#707172</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-note)"></div><b>--as-ink-note</b><i>#85858B</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-ph)"></div><b>--as-ink-ph</b><i>#BCBCC1</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-chev)"></div><b>--as-ink-chev</b><i>#C5C5C7</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-alert)"></div><b>--as-ink-alert</b><i>#838383</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-kbd)"></div><b>--as-ink-kbd</b><i>#494A4B</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-hint)"></div><b>--as-ink-hint</b><i>#CDCED0</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-off)"></div><b>--as-ink-off</b><i>#E5E6E9</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-glass)"></div><b>--as-ink-glass</b><i>rgba(255,255,255,.70)</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-glass-2)"></div><b>--as-ink-glass-2</b><i>rgba(255,255,255,.55)</i></div><div class="sw"><div class="chip" style="background:var(--as-ink-promo)"></div><b>--as-ink-promo</b><i>#FFFF7D</i></div><div class="sw"><div class="chip" style="background:var(--as-accent)"></div><b>--as-accent</b><i>#0082F7</i></div><div class="sw"><div class="chip" style="background:var(--as-accent-2)"></div><b>--as-accent-2</b><i>#0088FE</i></div><div class="sw"><div class="chip" style="background:var(--as-accent-3)"></div><b>--as-accent-3</b><i>#007AFF</i></div></div><h2>Radius</h2><div class="rad"><div><div class="rb" style="border-radius:26px"></div><em>26px</em></div><div><div class="rb" style="border-radius:38px"></div><em>38px</em></div><div><div class="rb" style="border-radius:34px"></div><em>34px</em></div><div><div class="rb" style="border-radius:18.5px"></div><em>18.5px</em></div><div><div class="rb" style="border-radius:999px"></div><em>999px</em></div><div><div class="rb" style="border-radius:6px"></div><em>6px</em></div><div><div class="rb" style="border-radius:28px"></div><em>28px</em></div></div><h2>Metrics</h2><div class="met">--as-w: 402px<br>--as-h: 874px<br>--as-status: 62px<br>--as-gutter: 20px<br>--as-icon: 62px<br>--as-bar-w: 361px<br>--as-bar-h: 62.7px<br>--as-bar-top: 790.5px<br>--as-tab-pitch: 68.7px</div></div>
</body>
</html>
Loading
Loading