- name
- apple-platform-guide
- description
- The developer-facing guide to building Apple UI (iOS 26, iPadOS 26, macOS 26) with
asset_pipeline's UI::View system. Backed by the Apple HIG corpus and validated by
macOS + iOS screenshots. Answers "I want to show X — what component, what args,
what HIG says."
- user-invocable
- true
- allowed-tools
- Read, Write, Edit, Glob, Grep, Bash
- version
- 0.1.0
# Apple Platform Guide
The developer-facing guide to building Apple UI with asset_pipeline, backed by the
HIG corpus and validated by screenshots. For every Apple HIG component there is a
matching `components/<slug>.md` answering: what `UI::View` to use, a quickstart,
the customization knobs, platform differences, and HIG citations. The `validation/`
tree holds the audit trail (HIG reference + rendered screenshots + prose verdict)
that proves each component doc is honest — it's the quality bar, not the primary
output. The primary output is the usage docs.
## Three-tier structure
```
apple-platform-guide/
SKILL.md (you are here)
foundations/ cross-cutting design concepts, hand-curated
liquid-glass.md
materials.md
hierarchy-and-emphasis.md
color-and-theming.md
typography.md
spacing-and-layout.md
preview-composition.md
preview-screen-recipes.md
accessibility-defaults.md
sound-and-motion.md
components/ one doc per UI::View, generated by the loop
buttons.md (not yet — generated per iteration)
popovers.md
...
recipes/ multi-component patterns, emergent
popover-with-glass.md
...
validation/ audit trail
worklist.json Ralph loop state machine
screenshots/ <slug>-macos.png + <slug>-ios.png
reports/ one markdown verdict per slug
gaps.md human-review queue
README.md
```
`foundations/` is read once. `components/` is where you look up how to build X.
`recipes/` is for cross-component patterns. `validation/` is the audit trail.
## Decision-tree lookup: "I want to show…"
Use this table to pick the right starting doc. Descend into the linked component
doc for constructor signatures, theming knobs, and HIG citations.
| Intent | Go to |
|--------|-------|
| …a button that triggers an instantaneous action | `components/buttons.md` |
| …a transient menu from a button (contextual actions) | `components/menu-button.md` |
| …a focused inspector anchored to an element | `components/popovers.md` |
| …a full-screen overlay that needs commit/cancel | `components/sheets.md` |
| …an urgent decision the user must acknowledge | `components/alerts.md` |
| …a blurred panel behind foreground controls | `foundations/materials.md` + `components/card.md` |
| …a Liquid Glass background on a custom view | `foundations/liquid-glass.md` |
| …a validation screenshot or HIG preview stage | `foundations/preview-composition.md` |
| …the exact screen to render for a component preview | `foundations/preview-screen-recipes.md` |
| …vertical or horizontal layout of child views | `components/vstack.md` / `components/hstack.md` |
| …a toolbar across the top or leading edge | `components/toolbar.md` |
| …a scrollable list of items | `components/list-view.md` |
| …editable single-line text input | `components/text-field.md` |
| …a selection between 2–5 mutually exclusive options | `components/segmented-control.md` |
| …a binary on/off control | `components/toggle.md` |
If your intent isn't in the table, consult `../apple-hig/pages/` (the raw HIG
corpus) to find the closest matching component page, then look up that slug.
## Quickstart snippets
Three canonical snippets. Each compiles with `crystal-alpha build ... -Dmacos`
or `-Dios`.
### 1. Button with primary visual weight + glass on iOS 26
`UI::Button` exposes `label`, `foreground_color`, `font`, `disabled`, `on_tap`
(source: `src/ui/views/button.cr`). A semantic `role` (`:primary | :cancel |
:destructive`) is planned; today wrap in `UI::GlassBackground` for primary weight.
```crystal
require "asset_pipeline/ui"
primary = UI::Button.new("Continue") { puts "tapped" }
primary.foreground_color = UI::Color.new(r: 1.0, g: 1.0, b: 1.0)
primary.font = UI::Font.new(size: 17.0, weight: :semibold)
glass_wrapped = UI::GlassBackground.new(content: primary, material: :regular)
glass_wrapped.corner_radius = 12.0
glass_wrapped.padding = UI::EdgeInsets.new(top: 10.0, trailing: 16.0, bottom: 10.0, leading: 16.0)
```
### 2. A popover anchored to a button with thin material
`UI::Popover` takes a content view and an `arrow_edge`; the companion
`UI::PopoverPresenter` owns the anchor-and-present relationship (see
`src/ui/views/popover.cr`).
```crystal
content = UI::VStack.new(spacing: 8.0)
content << UI::Label.new("Filter by")
content << UI::SegmentedControl.new(["All", "Unread"], 0)
popover = UI::Popover.new(content: content, arrow_edge: :top)
filter_btn = UI::Button.new("Filter")
presenter = UI::PopoverPresenter.new(popover, anchor: filter_btn)
filter_btn.on_tap = -> { presenter.present }
# The renderer wraps the popover content in a :thin-material glass layer
# on iOS 26 when you set the material explicitly:
glass = UI::GlassBackground.new(content: content, material: :thin)
popover.content = glass
```
### 3. A sheet with a toolbar header and confirm/cancel buttons
`UI::Sheet` carries a content view, detents, and a dismiss callback (see
`src/ui/views/sheet.cr`). Compose a toolbar header + action row inside the sheet's
content.
```crystal
header = UI::Toolbar.new("Edit Reminder")
header.add_item("cancel", "Cancel") { presenter.dismiss }
header.add_item("save", "Save") { save_and_dismiss }
body = UI::VStack.new(spacing: 12.0, alignment: UI::Alignment::Leading)
body << UI::TextField.new("Title")
body << UI::DatePicker.new
sheet_content = UI::VStack.new(spacing: 0.0)
sheet_content << header
sheet_content << body
sheet = UI::Sheet.new(content: sheet_content)
sheet.detents = [:medium, :large]
sheet.shows_drag_indicator = true
presenter = UI::SheetPresenter.new(sheet)
presenter.present
```
## Customization layers
There are three places to change how a component looks. Go to the leftmost option
that solves your problem.
1. **Per-instance arguments.** Constructor args and properties on the
`UI::View` itself. Example: `popover.arrow_edge = :leading`,
`button.foreground_color = Color.new(...)`. Always prefer this — it's scoped
to the one place that needs the change.
2. **`UI::Theme` tokens.** Global design tokens applied by the renderer to every
component. The full token list lives in `src/ui/theme.cr` — inspect it directly
for the canonical set. Current tokens include:
- Colors: `primary`, `on_primary`, `primary_container`, `secondary`, `error`,
`background`, `surface`, `outline` (plus `on_*` pairs for each).
- Typography: `font_family`, `font_size_body`, `font_size_title`,
`font_size_headline`, `font_size_caption`.
- Shape: `corner_radius_small`, `corner_radius_medium`, `corner_radius_large`.
Presets: `UI::Theme.apple_default` and `UI::Theme.material_baseline`. Component-
specific tokens like `popover_corner_radius` or `button_min_hit_target` are
planned but not yet in `theme.cr` — today those values are baked into the
renderer.
3. **Renderer metadata.** Platform-specific overrides for when a knob doesn't
exist anywhere else. This level means editing `appkit_renderer.cr` or
`uikit_renderer.cr` directly. Avoid unless you've checked both of the above and
confirmed there's no abstraction that fits. See `platform-renderers` skill.
## How to ask for a new customization
If you need a knob that doesn't exist:
1. **Describe the design intent in one sentence.** "I want popovers to feel more
formal — darker shadow, tighter corner." Design intent beats API wishlisting.
2. **Check `foundations/`** for a concept that already expresses this. If your
intent is "more formal / quieter", that might already live under
`hierarchy-and-emphasis.md` or `materials.md`. Reuse the vocabulary.
3. **Check `UI::Theme`** for an existing token. If your change is app-wide
(all popovers, all buttons), a theme token is the right layer — propose adding
one to `theme.cr` before adding a per-component property.
4. **If nothing fits, propose in a PR** with (a) the design intent, (b) a HIG
citation justifying that it's a real Apple concept not a personal preference,
(c) the proposed API shape, and (d) which layer (instance / theme / renderer)
it lives at.
## How to re-validate a component
When a `UI::View` changes or the HIG page is updated:
1. Open `validation/worklist.json`.
2. Find the row for the slug (e.g. `popovers`).
3. Flip `validation_state` back to `"pending"` and (optionally) clear
`docs_written` to force the usage doc to be regenerated too.
4. Re-run the Ralph loop with the `apple-platform-designer` agent. It will
re-render, re-screenshot, re-compare, and re-write both the report and the
component doc. Previous screenshots are overwritten; previous reports are
replaced.
## Cross-references
- `apple-hig` — the raw HIG corpus (166 pages + 1,724 images). Source of truth for
every citation.
- `ios26-native-components` — native SwiftUI / UIKit / AppKit class catalog with
iOS 26 Liquid Glass coverage.
- `glass-effects` — ObjC bridge details for `NSVisualEffectView` and
`UIVisualEffectView`. Go here when implementing a new glass-bearing view.
- `build-ui` — the Crystal component DSL for web rendering. `apple-platform-guide`
is the native-side analogue.
- `component-api` — the full API reference for every `UI::View` type, record, and
enum.
- `accessibility` — WCAG 2.2 AA compliance — focus, motion, contrast.
- `ax-test` — the macOS accessibility-tree test harness. Used by the validation
loop to confirm rendered components show up in the AX tree.
- `component-mapping-matrix` — the spine table that maps UI::View to SwiftUI /
UIKit / AppKit / Compose / Android / Web. A `HIG slug` column links rows here
back to `components/<slug>.md`.
## Regeneration
Worklist is generated (not hand-edited) by the triage script:
```
python3 ../apple-hig/_build/triage.py
```
That emits `validation/worklist.json` — one row per HIG component page with the
existing UI::View mapping, status, and priority. The Ralph loop then walks the
`pending` rows and asks the `apple-platform-designer` agent to process each:
```
/ralph-loop:ralph-loop \
"Use the apple-platform-designer agent to validate and document every HIG \
component. Output <promise>HIG_VALIDATION_COMPLETE</promise> when zero pending \
entries remain and every slug has docs_written: true." \
--completion-promise HIG_VALIDATION_COMPLETE
```
> **Slash-command syntax:** the Ralph loop ships as a plugin skill, so the
> command is `/ralph-loop:ralph-loop` (plugin name : skill name) — not
> `/ralph-loop` alone. Without the colon and second declaration the
> command fails with `Unknown skill: ralph-loop`.
See `.claude/agents/apple-platform-designer/agent.md` for the per-iteration
contract.
Auf GitHub ansehen