TL;DR: Seven-phase operating procedure for rolling a new Figma design revision into the component-spec docs: capture → delta → decisions → tokens/specs audit → doc roll-up → readiness audit → commit/PR. Every sync runs all phases — skipping one (most often tokens or the roll-up) is what causes doc drift. A sync is documentation-only; implementation is gated on Epic #2087. Start with Prerequisites (.fig decompression, MCP tooling) and check Gotchas before batch renames. When this sync is done, the natural next step is the Component Library Reconciliation Procedure (see Phase 7) — a separate question about the real shared library, not this doc's concern.
Figma source of record (verified 2026-07-30). The working file isowEYzHf7FrHRvWC2u82UOl("ALPA mobile app"). Start every capture there.psH738AqHDxuMyFm897f9ris a superseded file — it is whatdocs/ui-refresh-1821/figma-component-specs.jsonrecords in_resync.source.file(revision 2026-06-17), so any spec citing that key was captured against the older source and is unverified against current design.
Node ids are NOT reliably shared between the two files. Do not assume a node id captured from the superseded file resolves in the working file. Sampled 2026-07-30:4877:11274(advocacy) and4617:13876(card-duty period-alt) resolve;5048:13572(button pill) and5054:13801(notification card flight status) do not. Roughly half fail. Re-verify each id against the working file before relying on or rewriting a citation.
This is why per-componentfile key:citations acrossdocs/component-specifications/have not been bulk-rewritten to the working key — doing so would point readers at a file where the cited node may not resolve. See Typography Font Mapping § 5 for the worked example and the 25 affected docs.
Repeatable procedure for ingesting a new Figma export into the component specification docs. Every sync — regardless of scope — runs all phases in order. Skipping a phase is what causes docs to drift.
Implementation is gated. A sync updates documentation only — no RCL code, no .razor files, no ViewModel renaming in C#. Implementation is gated on Epic #2087 (Presentation Extraction).
A Figma sync is not just component naming. Every sync must cover four things:
| Area | What gets updated |
|---|---|
| Inventory | figma-component-specs.json — master component list |
| Tokens + specs | design-tokens.html — colors, typography, dimensions, MEC theming |
| Screen mapping | screen-mapping.html — which components appear where, overlays, tablet |
| Doc roll-up | index.html, architecture.html, domain-controls.html, naming-decisions-record.html |
Missing tokens or skipping the roll-up is the most common drift source.
.fig archive)macOS blocks shell access to ~/Downloads and ~/Desktop. Move the .fig export before starting:
mv ~/Downloads/ALPAmobileWtablet_YYYYMMDD ~/ALPAmobileWtablet_YYYYMMDD
A .fig file is a zip archive. The canvas blob uses DEFLATE (schema) + zstd (data tree).
cp ~/ALPAmobileWtablet_YYYYMMDD /tmp/figma_export.zip
cd /tmp && unzip figma_export.zip -d figma_export/
# Find the canvas blob (largest file)
ls -lh figma_export/
# Decompress zstd data layer → binary canvas
zstd -d figma_export/canvas.fig -o /tmp/canvas_data.bin
# Result: ~78 MB uncompressed binary
The sync uses the Figma MCP server (server name figma, configured per-machine in ~/.claude/mcp.json) for live API access to published files; the relevant operations are get_metadata, get_variable_defs, and get_screenshot. The MCP server cannot surface Figma Dev-Mode annotation pins — those require the binary .fig extraction above.
Rule: capture first, never overwrite decisions. Flag every collision with _flag; resolve nothing yet.
Using the MCP server, enumerate all top-level component masters. Exclude variant nodes (Property 1=*, State=*, Theme=*) — count masters only.
get_metadata → list all frames at canvas top level
→ filter: exclude variant property nodes
→ record: name, nodeId, width, height, fill, stroke
Compare against docs/ui-refresh-1821/figma-component-specs.json:
Compare against the full JSON body, not just_resync.dispositions. That top-of-file summary table is a point-in-time snapshot (capped at whatever D-number existed when it was last regenerated) — it does not reflect every decision resolved since. A 2026-07-13 sync pass compared only against that stale summary and flagged 4 of 5 "new masters" that were already fully resolved D-numbered entries elsewhere in the same file (see D50's "What Was Applied" section for the full story). Before flagging anything as new/renamed/removed, grep the whole file for the node id and the component name — a top-level key with a_noteciting a D-number means it's already dispositioned, even if_resync.dispositionsdoesn't mention it.
_flag: "new — tier and naming TBD"_flag: "name changed from X — resolve against decisions record"_flag: "not found in this revision — confirm retired or renamed"Dev-Mode pins and designer annotations are embedded as UTF-8 strings in the binary canvas:
python3 -c "
import re, sys
data = open('/tmp/canvas_data.bin','rb').read()
for m in re.finditer(rb'[\x20-\x7e]{30,}', data):
print(hex(m.start()), m.group().decode('ascii','ignore'))
" | grep -i 'restyle\|tablet\|swipe\|token\|theme\|overlay\|mec\|defer' | head -60
Record: offset address, verbatim text, inferred subject. These become the authoritative designer intent notes in the relevant spec doc.
python3 -c "
import re, sys
data = open('/tmp/canvas_data.bin','rb').read()
m = re.search(rb'tokens-themes_meta.*?}', data)
if m: print(m.group().decode('utf-8','ignore'))
"
Expected: {"core":"source","light":"enabled","dark":"disabled","theme":"enabled"} — the theme set is the MEC customization layer.
Component masters are not the whole design system. The dedicated foundation pages carry the canonical color ramp, type scale, spacing scale, grid, button system and form elements — and were historically never diffed against the docs, which is how foundation discrepancies (and the false "no spacing panel" note) went undetected through multiple syncs and audit passes. Every sync must capture each foundation page by node id and diff it against the docs:
| Foundation page | Node | Diff against |
|---|---|---|
| 🌈 Colors | 2093:566 | design-tokens.html color palette + theme-endpoint-contract Surface/* |
| Typography | obtain node URL from design | design-tokens.html type scale |
| 📐 Grid, Columns, Spacing | 1:17 | design-tokens.html spacing scale + grid |
| 🎛️ Buttons | 1:16 | ActionButton + button tokens |
| ☑️ Form Elements | 2093:702 | component-library.html / form specs |
| 🖤 Icons | 3:7 | asset-inventory.html icon catalog |
get_metadata(nodeId=<foundation page>) # structure: names, groups, sizes
get_design_context / get_variable_defs # exact hex / px / weight values
→ record discrepancies in foundations-reconciliation.html (capture-only; do NOT change live token/CSS values mid-sync)
Output target: foundations-reconciliation.html.
Note: get_variable_defs may require a live selection in the Figma desktop app; if it returns "nothing
selected," pull values via get_design_context instead, or flag the value as a capture gap.
The SOP was docs-only and never exported image assets or icon vectors, so the custom ALPA icon set was never inventoried and the docs carried a wrong "icons = FontAwesome/Material" assumption. Every sync must export and reconcile assets:
3:7 → "General Use Icons" 233:302) and any image assets; record
each in asset-inventory.html as the current
(interim) vector set.download_assets to wwwroot/icons/ as additive SVGs.
Never overwrite assets referenced by the mock UI (e.g. committees.svg,
phone.svg — referenced by MockWidgetApiService); a style-only re-export silently
changed them once. Cherry-pick new files; restore any clobbered mock-linked asset.A MEC airline page is not just a token/color capture. An investigation on 2026-07-06 found
ual-mec-spec.html had only ever recorded United's color/font tokens — the page's actual frames and
image assets were never inventoried, even though the Figma page (node 20657:502) carried 14-15 real
frames and 29 image-filled nodes, including branded icon instances (united/committee,
united/hotel) and a full Home-feed mock (Home-banner demo alt) showing a domain control
placed in live branded context. This is the same failure mode as the base-design foundations gap (§3.4) — a doc
that is internally consistent but was never checked against the full Figma source. Every MEC sync, whether for a
new airline or a re-check of an existing one, must capture both of these, not just token values:
figma-component-specs.json records base-design masters.{airline}/committee, {airline}/hotel) and any domain-control-in-context mock
(a full feed/screen frame showing how a real component renders with that airline's branding) — record per §3.5's
pattern, additive only, never overwriting a mock-linked asset.get_screenshot pass (or design-team confirmation) is required before writing a new hex value into
a MEC spec page as fact, the same way a value flagged only from raw fills stays a candidate, not an adopted
token.Output target: each airline's own spec page (docs/component-specifications/mec/{mecId}-mec-spec.html)
plus the MEC Onboarding Hub status table/badges.
Compare the Phase 1 inventory against every existing spec doc. Flag (do not resolve) anything that conflicts.
| Collision type | Example | Action |
|---|---|---|
| Name change | flight segment card → flight card-og | _flag in JSON, note in decisions record |
| New component | card-button not in current JSON | Add entry, flag for tier decision |
| Classification conflict | Component listed as scaffold in one doc, domain control in another | Flag both locations |
| Dimension discrepancy | Node measures 353×111 but doc says 360×120 | Flag in design-tokens.html |
| Retired component | Node no longer present in Figma | Flag for explicit retirement confirmation |
All _flag fields must be resolved (converted to _note) before Phase 5.
Every _flag requires a named decision (D-number). Do not proceed to Phase 4 with open flags.
naming-decisions-record.html — highest authority; adopted decisions are final*-property-mapping.md files — for per-field namingnaming-alignment-report.html — historical reference onlynaming-decisions-record.html)naming-decisions-record.html under the appropriate section_flag → _note in figma-component-specs.json citing the D-number and dateWhen renaming a Figma term across docs, exclude these two files from batch renames — they contain historical "Previous Name" columns that must not be overwritten:
naming-decisions-record.htmlnaming-alignment-report.htmlThe most easily skipped phase. Tokens go stale between syncs; always re-check.
Pull the text style nodes and compare against design-tokens.html. New type styles → add to font size/weight tables. Tablet breakpoint deltas → update the Tablet Typography Mode table.
python3 -c "
import re
data = open('/tmp/canvas_data.bin','rb').read()
for m in re.finditer(rb'(?:Surface|Border|Text|Fills|Accents|Status)/[A-Za-z0-9/_\-]+', data):
print(m.group().decode())
" | sort -u
Diff against the token tables in design-tokens.html. New tokens → add rows.
MEC theming tokens — the theme set defines the MEC customization layer. These four semantic hooks must always be present:
| Token | Purpose |
|---|---|
Surface/Brand | MEC primary surface (nav bar, card header tint) |
Border/Brand | MEC accent border |
Text/On-Brand | Text on brand surfaces |
Accents/Blue, Accents/Indigo, Accents/Red | Interactive accent colors |
get_metadata(nodeId) → width, height, fills, strokes, strokeWeight
Update the Component Dimensions table in design-tokens.html. Mark null sizes as TBD — sizing deep-dive pending.
Surface tokens are CSS-aligned for the Razor/Blazor path. XAML mapping is deferred to the native-XAML (Track B) surface work on ALPAMobile.Presentation — the shared library itself shipped as a Blazor RCL 2026-07-15 (D61); "scaffold PCL" was the planning-era shorthand:
StaticResource in XAML ResourceDictionaryDynamicResource (runtime swap via MergedDictionaries)AlpaTheme.xaml + MecTheme.xaml; only overridden tokens go in the MEC dictSurface/LIght (capital I) → normalize to SurfaceLight in all codeUpdate all hub documents to reflect the resolved decisions and new specs. Every file in this list must be touched — even if only to update a date.
| File | What to update |
|---|---|
figma-component-specs.json | All _flag → _note. No open flags remaining. |
naming-decisions-record.html | Every new D-number has a full adopted-decision block; status banner updated. |
design-tokens.html | Source comment updated, new CSS variables added, token tables updated, footer updated. |
screen-mapping.html | Component Inventory table updated, per-screen sections updated, Node Registry JSON updated. |
index.html | Scaffold component count, inline ViewModel tree, new component grid card. |
architecture.html | Components count, hierarchy tree, ViewModel reference table, domain controls table. |
domain-controls.html | Header count, catalog table, full .control profile for each new domain control. |
| New component spec pages | Create docs/component-specifications/<name>/<name>-component.html following the small-card pattern. |
After adopting a decision, grep for the old value everywhere — not just in the docs the decision text happens to name. A recurring failure mode: a decision resolves a stale node id or value in prose (and updates the doc it was raised against), but the same stale id/value survives untouched in 2-3 other docs that independently cited it — nobody grepped for it elsewhere. This happened three separate times before D50 (2026-07-13): a dated-.fig-snapshot node id lived on infigma-component-specs.jsonandscreen-mapping.htmlfor weeks afterdesign-tokens.htmland a component spec page had already moved on to the live node id. Before closing Phase 5, rungrep -rn "<old-id-or-value>"across all ofdocs/component-specifications/anddocs/ui-refresh-1821/, not just the files listed in the decision's own "Status" line.
Run this before sharing any doc set with the team. Seven passes, in order.
Every D-number mentioned in prose must have an <a href> linking to the exact anchor in naming-decisions-record.html and must reference an adopted decision.
grep -rn 'D[0-9]\{1,2\}[^0-9"#]' docs/component-specifications/ \
--include="*.html" | grep -v 'href=\|naming-decisions-record\|id="d'
Cross-check the same fact wherever it appears in multiple docs:
flight segment card (retired D15) only appears in historical context with an explicit "retired" labelVerify every doc that touches a resolved decision reflects adopted status. Check: figma-component-specs.json, naming-decisions-record.html, screen-mapping.html, design-tokens.html, domain-controls.html, and all component spec pages.
Every incomplete item must be explicitly marked. TBD / spec pending only on genuinely unspecced items. No resolved item accidentally left with a pending marker.
| Use this | Not this |
|---|---|
ButtonCard / ButtonCardViewModel | "CTA Card", "button-card component", "card-button" (Figma provenance only) |
FlightCard / FlightCardFactory | "flight card-og" as a code reference |
| "scaffold component" | "base component", "generic component", "template" |
| "domain control" | "feature component", "screen component" |
| "composes" (domain control → scaffold) | "extends", "subclasses", "wraps" |
Every doc that mentions another doc links to it. Every doc that cites a decision links to the decisions record anchor. Work item references are consistent and explained with enough context for a reader without Azure Boards access.
Validate the docs against the Figma source, not just against each other. For each foundation page
captured in 3.4, confirm the doc reflects the canonical frame —
catch claims that are internally consistent but wrong about Figma (e.g. "no spacing panel exists" when
117:176 proves otherwise; "icons = FontAwesome/Material" when 3:7 is a custom vector set).
# Tag balance check — run on every changed HTML file
python3 -c "
import re, sys
for path in sys.argv[1:]:
html = open(path).read()
for tag in ['div','table','tbody','tr']:
o = len(re.findall(f'<{tag}[\s>]', html, re.I))
c = len(re.findall(f'</{tag}>', html, re.I))
if o != c:
print(f'MISMATCH {path}: <{tag}> open={o} close={c}')
" docs/component-specifications/*.html
# JSON validity
python3 -m json.tool docs/ui-refresh-1821/figma-component-specs.json > /dev/null && echo "JSON OK"
# Work on a dedicated branch
git worktree add "../ALPA Mobile.worktrees/<sync-name>" -b docs/<sync-name>
# Stage specific files — never git add -A
git add docs/component-specifications/... docs/ui-refresh-1821/figma-component-specs.json
# Commit message format
git commit -m "docs: ingest <FigmaArchiveName> (<decisions resolved>)"
PRs go to Azure DevOps. Target both docs/component-specs-sizing and DotNet10:
az repos pr create --target-branch docs/component-specs-sizing \
--title "docs: <description>" --description "<body>"
Natural next step, not part of this procedure: once this sync's
doc roll-up lands, the spec docs may now describe components the real shared library doesn't have yet, or
confirm ones it does. Whether those changes should roll into
ALPAMobile.Presentation/Components/Library/, and whether anything newly specced deserves to become a
shared component, is a separate question this procedure doesn't answer — run the
Component Library Reconciliation Procedure
next.
| File | Phase | Purpose |
|---|---|---|
docs/ui-refresh-1821/figma-component-specs.json | 1, 2, 3 | Machine-readable master component inventory |
docs/component-specifications/naming-decisions-record.html | 3 | Authoritative D-number decision log |
docs/component-specifications/design-tokens.html | 4 | Colors, typography, dimensions, MEC theming |
docs/component-specifications/screen-mapping.html | 4, 5 | Per-screen component placement, node registry |
docs/component-specifications/index.html | 5 | Scaffold component grid and inline ViewModel tree |
docs/component-specifications/architecture.html | 5 | Full ViewModel hierarchy and domain controls summary |
docs/component-specifications/domain-controls.html | 5 | Domain control catalog and full profiles |
docs/component-specifications/<name>/<name>-component.html | 5 | Per-component spec and preview page |
When a component name conflict exists between Figma and the existing docs, resolve in this order (highest authority first):
naming-decisions-record.html — an adopted D-number decision is final*-property-mapping.md — field-level namingnaming-alignment-report.html — historical reference; informs but does not override-og, -TABLET suffixes, spaces)Code names must be PascalCase and not carry Figma suffixes. See D13.
| Tier | Description | Examples |
|---|---|---|
scaffold/atom | Smallest reusable unit, no children | ButtonViewModel |
scaffold/primitive | Composed of atoms; still domain-free | CardViewModel, SmallCardViewModel |
scaffold/container | Holds a collection of components | CarouselViewModel, ListViewModel |
scaffold/composite | Scaffold component with a nested scaffold child | ButtonCardViewModel |
nav-chrome | Navigation structure | navBar-bottom, topNav |
template | Full phone/tablet screen layout | template-home, template-tablet |
domain-control | Maps domain data onto a scaffold via factory | Pilot Card, Flight Card |
screen-out | Screen-specific layout only, not reusable | One-off onboarding panels |
deferred-responsive | Tablet variant; deferred until responsive layout system (post #2087) | flight card-TABLET, template-tablet |
~/Downloads and ~/Desktop — always move the .fig file to ~/ before starting..fig binary only. Use the binary extraction approach in Phase 1.markdownlint MD060 failures are expected — existing docs fail MD060. Known, accepted deviation.sed renames must exclude two files — naming-decisions-record.html and naming-alignment-report.html have "Previous Name" columns that must not be batch-renamed.Surface/LIght — capital I, not lowercase. Normalize to SurfaceLight in all code output; never reproduce the typo in code identifiers.-TABLET suffix or in a template-tablet frame goes to deferred-responsive tier until post #2087..razor changes, no RCL work until #2087 is delivered.imp/blazor-hybrid by the time the PR is ready, since unrelated feature work merges
continuously. This surfaces late, as a merge-conflict scare on PR creation/push, not at branch-creation
time. Before starting multi-commit sync work in an existing worktree, run
git fetch origin imp/blazor-hybrid && git log --oneline HEAD..origin/imp/blazor-hybrid | wc -l —
if it's more than a handful of commits, rebase onto the current tip before doing the sync work,
not after. Resolve with a rebase (replays only your commits), never a full merge of a
stale branch (drags in every unrelated commit in between and produces a huge, unreviewable diff).