forked from bchanot/claude
chore(skills): sync design-motion-principles from upstream (make update)
Vendored-skill content refreshed by update-all.sh step 8 during the soak update runs: demo-shell + output-format reworked upstream, new report-template.html reference. Content-only, no wiring change. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XpphkdTosUzokBDNG7PToS
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
3049250150
commit
82ce02cf28
@@ -1,63 +1,77 @@
|
||||
<!--
|
||||
Demo Shell — design-motion-principles v2.1
|
||||
===========================================
|
||||
Demo Shell — design-motion-principles
|
||||
=====================================
|
||||
|
||||
This file is a template the audit agent reads during STEP 3 of the
|
||||
audit workflow (see ../SKILL.md). The agent embeds one .demo-card per
|
||||
Critical or Important finding (per R4 in the plan — Opportunities do
|
||||
not get demo cards).
|
||||
Minimal, isolated reference for a single demo card. The agent reads this
|
||||
during STEP 3 of the audit workflow (see ../SKILL.md) and uses it as the
|
||||
per-finding template — one .demo block per Critical or Important finding
|
||||
(Opportunities don't get demo cards).
|
||||
|
||||
How to use this file as the agent
|
||||
---------------------------------
|
||||
The full worked example with five demo cards in context lives in
|
||||
references/report-template.html. This file is intentionally minimal:
|
||||
one card, every contract visible, no report scaffolding.
|
||||
|
||||
How the agent uses this file
|
||||
----------------------------
|
||||
1. Copy the entire <style> block into the report's <head>. The shell's
|
||||
CSS variables, .demo-card layout, loop indicator, and the
|
||||
prefers-reduced-motion guard are shared across all demo cards in
|
||||
the report — they are not duplicated per finding.
|
||||
tokens, .demo layout, stage colors, segmented control, and the
|
||||
prefers-reduced-motion guard are SHARED across every demo card in the
|
||||
report and must not be duplicated per finding.
|
||||
|
||||
2. For each finding {n} (1-indexed across the whole report):
|
||||
a. Replace the MOTION-CODE-SLOT-{n} comment in <style> with the
|
||||
per-finding @keyframes block AND any .demo-card-{n}__motion-target
|
||||
selector rules. Use the suffix {n} so multiple findings in one
|
||||
report do not collide on keyframe names or target selectors.
|
||||
b. Replace the DEMO-CARD-MOTION-SLOT-{n} comment in the .demo-card
|
||||
a. Replace the MOTION-CODE-SLOT-{n} comment with the per-finding
|
||||
@keyframes m{n} block AND the .demo-{n}__mt selector rule.
|
||||
Suffix {n} so multiple findings cannot collide on keyframe
|
||||
names or selectors.
|
||||
b. Replace the DEMO-CARD-MOTION-SLOT-{n} comment in the .demo__stage
|
||||
markup with the actual motion-target element. Its class must be
|
||||
.demo-card-{n}__motion-target so it matches the rules above.
|
||||
c. Set the .demo-card__header text to a short title for the
|
||||
recommended motion (e.g., "Subtle enter: opacity + translateY + blur").
|
||||
d. Set the .demo-card__subhead text to the duration plus easing or
|
||||
other relevant values (e.g., "300ms · ease-out"). The subhead
|
||||
ALWAYS renders — the agent populates it for every demo so card
|
||||
heights stay consistent across the report.
|
||||
.demo-{n}__mt so it matches the rules above.
|
||||
c. Set .demo__title to a short title for the recommended motion
|
||||
(e.g., "Quick tab crossfade", "Sheet enter (mirror for exit)").
|
||||
d. Set .demo__timing to the duration plus easing (e.g.,
|
||||
"180ms · ease-out", "300ms · ease-out-quint"). The subhead
|
||||
ALWAYS renders — populate it for every demo so card heights
|
||||
stay consistent across the report.
|
||||
e. Renumber the four radio ids from st1-* to st{n}-*, and the
|
||||
labels' for= attributes to match.
|
||||
|
||||
3. Per-finding code MUST honor these contracts:
|
||||
- Do not redefine the shell's CSS variables (--bg, --fg, --border,
|
||||
--accent, --loop-dim, --card-radius, --card-padding, --gap,
|
||||
- Do NOT redefine the shell's tokens (--ink, --paper, --surface-2,
|
||||
--line, --line-strong, --st-bg, --st-fg, --st-line, --st-dim,
|
||||
--sans, --mono). Use them via var().
|
||||
- Do not modify the prefers-reduced-motion block. The shell's
|
||||
guard collapses all .demo-card-{n}__motion-target animations to
|
||||
none. The per-finding @keyframes 100% state must match the
|
||||
motion-target's default rendered state so the reduce-motion
|
||||
fallback shows the correct final visual.
|
||||
- Per-finding @keyframes use the 0% / 66% / 100% cadence:
|
||||
0% = start state, 66% = motion complete (~2s in), 100% = hold
|
||||
(~1s). The shell uses animation-duration: 3s.
|
||||
- Do NOT modify the prefers-reduced-motion block. The shell's
|
||||
guard collapses all .demo-{n}__mt animations to none. The
|
||||
per-finding @keyframes 100% state MUST match the motion-target
|
||||
element's default static rendering so the reduce-motion fallback
|
||||
shows the correct final visual.
|
||||
- Per-finding @keyframes use the 0% / ~60% / 100% cadence:
|
||||
0% = start state, ~60% = motion complete (~1.8s in),
|
||||
100% = hold (~1.2s). The shell uses animation-duration: 3s.
|
||||
|
||||
4. Demo cards are non-interactive. They have no hover or focus state
|
||||
beyond the default outline suppression. tabindex="-1" keeps them
|
||||
out of keyboard nav order — readers tab through findings, not
|
||||
through demo cards.
|
||||
4. Motion targets and ANY UI primitives inside the stage (.ui-btn,
|
||||
.ui-card, .ui-row, .ui-check, .ui-num, .ui-label, badge, etc.)
|
||||
use --st-bg / --st-fg / --st-line / --st-dim — NEVER --accent or
|
||||
other page tokens. This guarantees the demo contrasts correctly
|
||||
when its stage is locked to a different theme than the report.
|
||||
|
||||
5. Demo cards are non-interactive. The stage toggle radios are the
|
||||
only interactive element. tabindex on the radios is fine; the
|
||||
stage itself stays out of focus order.
|
||||
|
||||
Loop pacing
|
||||
-----------
|
||||
animation-duration: 3s. Keyframes 0% / 66% / 100%. Motion 0-66% = ~2s,
|
||||
hold 66-100% = ~1s, then the animation restarts. Per-finding code
|
||||
imitates this cadence so all demos in a report share the same rhythm.
|
||||
animation-duration: 3s. Keyframes 0% / ~60% / 100%. Motion 0–60% ≈ 1.8s,
|
||||
hold 60–100% ≈ 1.2s, then restart. Every demo in a report uses this
|
||||
cadence so all cards share the same rhythm.
|
||||
|
||||
Empty state (this file rendered standalone)
|
||||
-------------------------------------------
|
||||
Opening this file directly in a browser shows one .demo-card with the
|
||||
loop indicator and placeholder content. No motion plays — the agent
|
||||
injects motion per finding when this template is embedded in a report.
|
||||
Standalone preview
|
||||
------------------
|
||||
Opening this file directly in a browser shows one .demo card with the
|
||||
loop indicator, the segmented Auto/Light/Dark stage toggle, and a
|
||||
placeholder motion target (a simple shape that fades in). No real
|
||||
per-finding motion plays — the agent injects motion per finding when
|
||||
this template is embedded in a report. The shell also honors the
|
||||
viewer's prefers-color-scheme for standalone rendering.
|
||||
-->
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
@@ -65,144 +79,197 @@
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title>Demo Shell — design-motion-principles</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Familjen+Grotesk:wght@400;500;600;700&family=Public+Sans:wght@400;500;600;700&family=Geist+Mono:wght@400;500;600&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--bg: #ffffff;
|
||||
--fg: #111111;
|
||||
--border: #e5e5e5;
|
||||
--accent: #111111;
|
||||
--loop-dim: rgba(0, 0, 0, 0.45);
|
||||
--card-radius: 12px;
|
||||
--card-padding: 20px;
|
||||
--gap: 12px;
|
||||
--sans: system-ui, -apple-system, "Segoe UI", sans-serif;
|
||||
--mono: ui-monospace, "SF Mono", Menlo, monospace;
|
||||
/* Cool slate-graphite neutrals (hue 255). In the full report these
|
||||
are overridden by a global theme toggle; here the shell flips via
|
||||
prefers-color-scheme for standalone preview. */
|
||||
--ink: oklch(0.155 0.006 255);
|
||||
--surface: oklch(0.195 0.007 255);
|
||||
--surface-2: oklch(0.235 0.008 255);
|
||||
--paper: oklch(0.945 0.004 255);
|
||||
--paper-dim: oklch(0.800 0.006 255);
|
||||
--muted: oklch(0.680 0.008 255);
|
||||
--faint: oklch(0.505 0.008 255);
|
||||
--line: oklch(0.95 0.01 255 / 0.09);
|
||||
--line-strong:oklch(0.95 0.01 255 / 0.16);
|
||||
|
||||
--display: "Familjen Grotesk", "Hanken Grotesk", system-ui, sans-serif;
|
||||
--sans: "Public Sans", -apple-system, BlinkMacSystemFont, "Segoe UI", system-ui, sans-serif;
|
||||
--mono: "Geist Mono", ui-monospace, "SF Mono", Menlo, monospace;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
@media (prefers-color-scheme: light) {
|
||||
:root {
|
||||
--bg: #1a1a1a;
|
||||
--fg: #f0f0f0;
|
||||
--border: #333333;
|
||||
--accent: #f0f0f0;
|
||||
--loop-dim: rgba(240, 240, 240, 0.45);
|
||||
--ink: oklch(0.985 0.003 255);
|
||||
--surface: oklch(0.965 0.004 255);
|
||||
--surface-2: oklch(0.940 0.005 255);
|
||||
--paper: oklch(0.180 0.006 255);
|
||||
--paper-dim: oklch(0.330 0.008 255);
|
||||
--muted: oklch(0.475 0.010 255);
|
||||
--faint: oklch(0.640 0.009 255);
|
||||
--line: oklch(0.180 0.010 255 / 0.12);
|
||||
--line-strong:oklch(0.180 0.010 255 / 0.22);
|
||||
}
|
||||
}
|
||||
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
background: var(--ink);
|
||||
color: var(--paper);
|
||||
font-family: var(--sans);
|
||||
margin: 0;
|
||||
padding: 32px;
|
||||
}
|
||||
|
||||
.demo-card {
|
||||
position: relative;
|
||||
background: var(--bg);
|
||||
color: var(--fg);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--card-radius);
|
||||
padding: var(--card-padding);
|
||||
max-width: 360px;
|
||||
min-width: 280px;
|
||||
font-family: var(--sans);
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.demo-card:focus-visible {
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.demo-card__header {
|
||||
font-size: 0.875rem;
|
||||
font-weight: 600;
|
||||
color: var(--fg);
|
||||
margin-bottom: 4px;
|
||||
padding-right: 72px;
|
||||
}
|
||||
|
||||
.demo-card__subhead {
|
||||
font-family: var(--mono);
|
||||
font-size: 0.75rem;
|
||||
color: var(--loop-dim);
|
||||
margin-bottom: var(--gap);
|
||||
}
|
||||
|
||||
.demo-card__stage {
|
||||
background: var(--bg);
|
||||
border: 1px dashed var(--border);
|
||||
border-radius: 8px;
|
||||
padding: 24px;
|
||||
min-height: 100px;
|
||||
padding: 48px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.demo-card__loop-indicator {
|
||||
position: absolute;
|
||||
top: 12px;
|
||||
right: 12px;
|
||||
font-family: var(--mono);
|
||||
font-size: 0.7rem;
|
||||
color: var(--loop-dim);
|
||||
letter-spacing: 0.02em;
|
||||
user-select: none;
|
||||
/* visually-hidden (stage-toggle radios) */
|
||||
.vh { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
|
||||
|
||||
/* ───────────── Demo card ───────────── */
|
||||
.demo { position: relative; width: 380px; }
|
||||
.demo__bar { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; margin-bottom: 12px; }
|
||||
.demo__meta { min-width: 0; }
|
||||
.demo__title { display: block; font-size: 13.5px; font-weight: 600; color: var(--paper); line-height: 1.3; }
|
||||
.demo__timing { display: block; font-family: var(--mono); font-size: 11px; color: var(--muted); margin-top: 2px; font-variant-numeric: tabular-nums; }
|
||||
.demo__controls { display: flex; align-items: center; gap: 12px; flex-shrink: 0; }
|
||||
.demo__loop { font-family: var(--mono); font-size: 12px; color: var(--faint); }
|
||||
|
||||
/* segmented Auto / Light / Dark stage toggle (pure CSS).
|
||||
Auto = follow ambient theme via --surface-2 + --paper.
|
||||
Light / Dark = locked overrides (hardcoded values, ignore ambient). */
|
||||
.seg { display: inline-flex; border: 1px solid var(--line-strong); border-radius: 100px; overflow: hidden; }
|
||||
.seg label { font-family: var(--mono); font-size: 9.5px; letter-spacing: 0.1em; text-transform: uppercase; color: var(--muted); padding: 4px 9px; cursor: pointer; user-select: none; transition: background 0.15s ease, color 0.15s ease; }
|
||||
.demo input[id$="-a"]:checked ~ .demo__bar .seg label[for$="-a"],
|
||||
.demo input[id$="-l"]:checked ~ .demo__bar .seg label[for$="-l"],
|
||||
.demo input[id$="-d"]:checked ~ .demo__bar .seg label[for$="-d"] {
|
||||
background: color-mix(in oklch, var(--paper) 12%, transparent);
|
||||
color: var(--paper);
|
||||
}
|
||||
|
||||
.demo__stage {
|
||||
/* AUTO default — stage uses the ambient elevated surface */
|
||||
--st-bg: var(--surface-2);
|
||||
--st-fg: var(--paper);
|
||||
--st-line: var(--line-strong);
|
||||
--st-dim: color-mix(in oklch, var(--paper) 55%, transparent);
|
||||
background: var(--st-bg); color: var(--st-fg);
|
||||
border: 1px solid var(--line); border-radius: 10px;
|
||||
padding: 32px 24px; min-height: 168px;
|
||||
display: flex; align-items: center; justify-content: center; overflow: hidden;
|
||||
}
|
||||
/* Locked Light: hardcoded light, regardless of ambient theme */
|
||||
.demo input[id$="-l"]:checked ~ .demo__stage {
|
||||
--st-bg: oklch(0.985 0.003 255);
|
||||
--st-fg: oklch(0.180 0.006 255);
|
||||
--st-line: oklch(0.180 0.010 255 / 0.18);
|
||||
--st-dim: oklch(0.180 0.010 255 / 0.45);
|
||||
}
|
||||
/* Locked Dark: hardcoded dark, regardless of ambient theme */
|
||||
.demo input[id$="-d"]:checked ~ .demo__stage {
|
||||
--st-bg: oklch(0.180 0.006 255);
|
||||
--st-fg: oklch(0.985 0.003 255);
|
||||
--st-line: oklch(0.985 0.010 255 / 0.18);
|
||||
--st-dim: oklch(0.985 0.010 255 / 0.50);
|
||||
}
|
||||
|
||||
/* Generic UI primitives for use inside motion targets. All theme via
|
||||
stage tokens, NEVER page tokens — so they contrast correctly when a
|
||||
stage is locked to a different theme than the report. */
|
||||
.ui-btn { display: inline-flex; align-items: center; justify-content: center; padding: 9px 18px; border-radius: 8px; background: var(--st-fg); color: var(--st-bg); font-family: var(--sans); font-size: 13px; font-weight: 600; }
|
||||
.ui-card { background: var(--st-bg); border: 1px solid var(--st-line); border-radius: 10px; padding: 14px 16px; min-width: 180px; box-shadow: 0 8px 24px oklch(0 0 0 / 0.12); }
|
||||
.ui-row { display: flex; align-items: center; gap: 10px; font-size: 13px; color: var(--st-fg); }
|
||||
.ui-check { width: 22px; height: 22px; border-radius: 7px; background: var(--st-fg); display: inline-flex; align-items: center; justify-content: center; flex-shrink: 0; }
|
||||
.ui-check svg { width: 13px; height: 13px; stroke: var(--st-bg); }
|
||||
.ui-num { font-family: var(--display); font-size: 34px; font-weight: 700; color: var(--st-fg); font-variant-numeric: tabular-nums; letter-spacing: -0.02em; }
|
||||
.ui-label { font-family: var(--mono); font-size: 10px; letter-spacing: 0.12em; text-transform: uppercase; color: var(--st-dim); }
|
||||
|
||||
/* prefers-reduced-motion — disables every per-finding animation and
|
||||
hides the loop indicator. The per-finding @keyframes 100% state
|
||||
must match the motion-target's default rendering so the reduce
|
||||
fallback shows the correct final visual. */
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.demo-card__loop-indicator {
|
||||
display: none;
|
||||
}
|
||||
[class*="__motion-target"] {
|
||||
animation: none !important;
|
||||
transition: none !important;
|
||||
}
|
||||
.demo__loop { display: none; }
|
||||
[class*="__mt"], [class*="__mt"] * { animation: none !important; transition: none !important; }
|
||||
}
|
||||
|
||||
/* MOTION-CODE-SLOT-{n}
|
||||
Per-finding @keyframes and .demo-card-{n}__motion-target rules
|
||||
go here. {n} = finding's 1-indexed position across the report.
|
||||
Per-finding @keyframes and .demo-{n}__mt rules go here. {n} is the
|
||||
finding's 1-indexed position across the report.
|
||||
|
||||
Example shape (replace per finding):
|
||||
Example shape (replace per finding — wrap in
|
||||
@media (prefers-reduced-motion: no-preference) { ... }):
|
||||
|
||||
@keyframes motion-1-enter {
|
||||
0% { opacity: 0; transform: translateY(8px); filter: blur(4px); }
|
||||
66% { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
100% { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
}
|
||||
.demo-card-1__motion-target {
|
||||
animation: motion-1-enter 3s infinite;
|
||||
display: inline-block;
|
||||
padding: 8px 16px;
|
||||
border-radius: 6px;
|
||||
background: var(--accent);
|
||||
color: var(--bg);
|
||||
font-family: var(--sans);
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@keyframes m1 {
|
||||
0% { opacity: 0; transform: translateY(8px); filter: blur(4px); }
|
||||
60% { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
100% { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
}
|
||||
.demo-1__mt {
|
||||
animation: m1 3s cubic-bezier(0.22, 1, 0.36, 1) infinite;
|
||||
}
|
||||
}
|
||||
*/
|
||||
|
||||
/* Placeholder motion for standalone preview only — agent strips this
|
||||
when composing the report. Demonstrates the 0 / 60 / 100 cadence. */
|
||||
@media (prefers-reduced-motion: no-preference) {
|
||||
@keyframes m-placeholder {
|
||||
0% { opacity: 0; transform: translateY(8px); filter: blur(4px); }
|
||||
60% { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
100% { opacity: 1; transform: translateY(0); filter: blur(0); }
|
||||
}
|
||||
.demo-1__mt {
|
||||
animation: m-placeholder 3s cubic-bezier(0.22, 1, 0.36, 1) infinite;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<article class="demo-card" tabindex="-1">
|
||||
<div class="demo-card__loop-indicator">↻ looping</div>
|
||||
<div class="demo-card__header">Recommended motion title</div>
|
||||
<div class="demo-card__subhead">300ms · ease-out</div>
|
||||
<div class="demo-card__stage">
|
||||
<div class="demo">
|
||||
<input class="vh" type="radio" name="st1" id="st1-a" checked>
|
||||
<input class="vh" type="radio" name="st1" id="st1-l">
|
||||
<input class="vh" type="radio" name="st1" id="st1-d">
|
||||
<div class="demo__bar">
|
||||
<div class="demo__meta">
|
||||
<span class="demo__title">Recommended motion title</span>
|
||||
<span class="demo__timing">300ms · ease-out</span>
|
||||
</div>
|
||||
<div class="demo__controls">
|
||||
<div class="seg">
|
||||
<label for="st1-a">Auto</label>
|
||||
<label for="st1-l">Light</label>
|
||||
<label for="st1-d">Dark</label>
|
||||
</div>
|
||||
<span class="demo__loop">↻</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="demo__stage">
|
||||
<!-- DEMO-CARD-MOTION-SLOT-{n}
|
||||
Per-finding motion-target element goes here. Its class must
|
||||
match the rules in MOTION-CODE-SLOT-{n} above:
|
||||
<div class="demo-card-{n}__motion-target">...</div>
|
||||
<div class="demo-{n}__mt">...content...</div>
|
||||
The element's contents are agent-determined (a button shape,
|
||||
a card, an icon, a list of items for stagger demos, etc.). -->
|
||||
<span style="color: var(--loop-dim); font-family: var(--mono); font-size: 0.75rem;">
|
||||
(motion preview renders here per finding)
|
||||
</span>
|
||||
a card, an icon, a row of items for stagger demos, a number,
|
||||
a badge, etc.). All inner UI primitives use the stage tokens
|
||||
(--st-fg / --st-bg / --st-line / --st-dim) — NEVER page tokens. -->
|
||||
<div class="demo-1__mt">
|
||||
<div class="ui-card">
|
||||
<div class="ui-label" style="margin-bottom: 6px;">Placeholder</div>
|
||||
<div class="ui-row">
|
||||
<span class="ui-check"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3.5"><path d="M5 13l4 4L19 7"/></svg></span>
|
||||
<span>(motion preview renders here per finding)</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</div>
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
Reference in New Issue
Block a user