Add scan flow MVP and local Axiom skill workspace
This snapshot establishes the camera-to-result recognition flow and related tests while checking in the project skill/docs assets required for the configured local tooling.
This commit is contained in:
7
.claude/skills/axiom-typography-ref/.openskills.json
Normal file
7
.claude/skills/axiom-typography-ref/.openskills.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"source": "CharlesWiltgen/Axiom",
|
||||
"sourceType": "git",
|
||||
"repoUrl": "https://github.com/CharlesWiltgen/Axiom",
|
||||
"subpath": "axiom-codex/skills/axiom-typography-ref",
|
||||
"installedAt": "2026-04-12T08:06:54.451Z"
|
||||
}
|
||||
498
.claude/skills/axiom-typography-ref/SKILL.md
Normal file
498
.claude/skills/axiom-typography-ref/SKILL.md
Normal file
@@ -0,0 +1,498 @@
|
||||
---
|
||||
name: axiom-typography-ref
|
||||
description: Apple platform typography reference (San Francisco fonts, text styles, Dynamic Type, tracking, leading, internationalization) through iOS 26
|
||||
license: MIT
|
||||
---
|
||||
|
||||
# Typography Reference
|
||||
|
||||
Complete reference for typography on Apple platforms including San Francisco font system, text styles, Dynamic Type, tracking, leading, and internationalization through iOS 26.
|
||||
|
||||
## San Francisco Font System
|
||||
|
||||
### Font Families
|
||||
|
||||
**SF Pro** and **SF Pro Rounded** (iOS, iPadOS, macOS, tvOS)
|
||||
- Main system fonts for most UI elements
|
||||
- Rounded variant for friendly, approachable interfaces (e.g., Reminders app)
|
||||
|
||||
**SF Compact** and **SF Compact Rounded** (watchOS, narrow columns)
|
||||
- Optimized for constrained spaces and small sizes
|
||||
- watchOS default system font
|
||||
|
||||
**SF Mono** (Code environments, monospaced text)
|
||||
- Monospaced font for code editors and technical content
|
||||
- Consistent character widths for alignment
|
||||
|
||||
**New York** (Serif system font)
|
||||
- Serif alternative for editorial content
|
||||
- Works with text styles just like SF Pro
|
||||
|
||||
### Variable Font Axes
|
||||
|
||||
#### Weight Axis (9 weights)
|
||||
- Ultralight, Thin, Light, Regular, Medium, Semibold, Bold, Heavy, Black
|
||||
- Continuous weight spectrum via variable fonts
|
||||
- Avoid light weights at small sizes (legibility issues)
|
||||
|
||||
#### Width Axis (WWDC 2022)
|
||||
- **Condensed** — narrowest width
|
||||
- **Compressed** — narrow width
|
||||
- **Regular** — standard width (default)
|
||||
- **Expanded** — wide width
|
||||
|
||||
Access via:
|
||||
```swift
|
||||
// iOS/macOS
|
||||
let descriptor = UIFontDescriptor(fontAttributes: [
|
||||
.family: "SF Pro",
|
||||
kCTFontWidthTrait: 1.0 // 1.0 = Expanded
|
||||
])
|
||||
```
|
||||
|
||||
**SF Arabic** (WWDC 2022)
|
||||
- Matches SF Pro design language for Arabic text
|
||||
- Proper right-to-left support
|
||||
|
||||
#### Optical Sizes
|
||||
Variable fonts automatically adjust optical size based on point size:
|
||||
- **Text variant** (< 20pt) — more spacing, sturdier strokes
|
||||
- **Display variant** (≥ 20pt) — tighter spacing, refined details
|
||||
- **Smooth transition** (17-28pt) with variable SF Pro
|
||||
|
||||
From WWDC 2020:
|
||||
> "TextKit 2 abstracts away glyph handling to provide a consistent experience for international text."
|
||||
|
||||
## Text Styles & Dynamic Type
|
||||
|
||||
### System Text Styles
|
||||
|
||||
| Text Style | Default Size (iOS) | Use Case |
|
||||
|------------|-------------------|----------|
|
||||
| `.largeTitle` | 34pt | Primary page headings |
|
||||
| `.title` | 28pt | Secondary headings |
|
||||
| `.title2` | 22pt | Tertiary headings |
|
||||
| `.title3` | 20pt | Quaternary headings |
|
||||
| `.headline` | 17pt (Semibold) | Emphasized body text |
|
||||
| `.body` | 17pt | Primary body text |
|
||||
| `.callout` | 16pt | Secondary body text |
|
||||
| `.subheadline` | 15pt | Tertiary body text |
|
||||
| `.footnote` | 13pt | Footnotes, captions |
|
||||
| `.caption` | 12pt | Small annotations |
|
||||
| `.caption2` | 11pt | Smallest annotations |
|
||||
|
||||
#### Font Size Guidance
|
||||
|
||||
- **Avoid `.caption2` for readable content** — at 11pt, it's acceptable for timestamps and metadata annotations but too small for body text or labels users need to read. Prefer `.caption` or `.footnote` as the minimum for readable content.
|
||||
|
||||
### Emphasized Text Styles
|
||||
|
||||
Apply `.bold` symbolic trait to get emphasized variants:
|
||||
|
||||
```swift
|
||||
// UIKit
|
||||
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .title1)
|
||||
let boldDescriptor = descriptor.withSymbolicTraits(.traitBold)!
|
||||
let font = UIFont(descriptor: boldDescriptor, size: 0)
|
||||
|
||||
// SwiftUI
|
||||
Text("Bold Title")
|
||||
.font(.title.bold())
|
||||
```
|
||||
|
||||
**Actual weights by text style:**
|
||||
- Some styles map to **medium**
|
||||
- Others map to **semibold**, **bold**, or **heavy**
|
||||
- Depends on semantic hierarchy
|
||||
|
||||
### Leading Variants
|
||||
|
||||
**Tight Leading** (reduces line height by 2pt on iOS, 1pt on watchOS):
|
||||
```swift
|
||||
// UIKit
|
||||
let descriptor = UIFontDescriptor.preferredFontDescriptor(withTextStyle: .body)
|
||||
let tightDescriptor = descriptor.withSymbolicTraits(.traitTightLeading)!
|
||||
|
||||
// SwiftUI
|
||||
Text("Compact text")
|
||||
.font(.body.leading(.tight))
|
||||
```
|
||||
|
||||
**Loose Leading** (increases line height by 2pt on iOS, 1pt on watchOS):
|
||||
```swift
|
||||
// SwiftUI
|
||||
Text("Spacious paragraph")
|
||||
.font(.body.leading(.loose))
|
||||
```
|
||||
|
||||
### Dynamic Type
|
||||
|
||||
**Automatic Scaling** (iOS):
|
||||
Text styles scale automatically based on user preferences from Settings → Display & Brightness → Text Size.
|
||||
|
||||
**Custom Fonts with Dynamic Type:**
|
||||
|
||||
```swift
|
||||
// UIKit - UIFontMetrics
|
||||
let customFont = UIFont(name: "Avenir-Medium", size: 34)!
|
||||
let bodyMetrics = UIFontMetrics(forTextStyle: .body)
|
||||
let scaledFont = bodyMetrics.scaledFont(for: customFont)
|
||||
|
||||
// Also scale constants
|
||||
let spacing = bodyMetrics.scaledValue(for: 20.0)
|
||||
```
|
||||
|
||||
```swift
|
||||
// SwiftUI - .font(.custom(_:relativeTo:))
|
||||
Text("Custom scaled text")
|
||||
.font(.custom("Avenir-Medium", size: 34, relativeTo: .body))
|
||||
|
||||
// @ScaledMetric for values
|
||||
@ScaledMetric(relativeTo: .body) var padding: CGFloat = 20
|
||||
```
|
||||
|
||||
### Platform Differences
|
||||
|
||||
**macOS**
|
||||
- No Dynamic Type support in AppKit
|
||||
- Text style sizes optimized for macOS control sizes
|
||||
- Catalyst apps use iOS sizes × 77% (legacy) or macOS-optimized sizes ("Optimize Interface for Mac")
|
||||
|
||||
**watchOS**
|
||||
- Smaller text styles optimized for watch faces
|
||||
- Tight leading default for compact displays
|
||||
|
||||
**visionOS**
|
||||
- System fonts work identically to iOS
|
||||
- Dynamic Type support included
|
||||
|
||||
## Tracking & Leading
|
||||
|
||||
### Tracking (Letter Spacing)
|
||||
|
||||
Tracking adjusts space between letters. Essential for optical size behavior.
|
||||
|
||||
**Size-Specific Tracking Tables:**
|
||||
|
||||
SF Pro includes tracking values that vary by point size to maintain optimal spacing:
|
||||
- Larger sizes: tighter tracking
|
||||
- Smaller sizes: looser tracking
|
||||
|
||||
Example from Apple Design Resources:
|
||||
- 34pt (largeTitle): +0.016 tracking
|
||||
- 17pt (body): +0.008 tracking
|
||||
- 11pt (caption2): +0.06 tracking
|
||||
|
||||
**Tight Tracking API** (for fitting text):
|
||||
```swift
|
||||
// UIKit
|
||||
textView.allowsDefaultTightening(for: .byTruncatingTail)
|
||||
|
||||
// SwiftUI
|
||||
Text("Long text that needs to fit")
|
||||
.lineLimit(1)
|
||||
.minimumScaleFactor(0.5) // Allows tight tracking
|
||||
```
|
||||
|
||||
**Manual Tracking:**
|
||||
```swift
|
||||
// UIKit
|
||||
let attributes: [NSAttributedString.Key: Any] = [
|
||||
.font: UIFont.preferredFont(forTextStyle: .body),
|
||||
.kern: 2.0 // 2pt tracking
|
||||
]
|
||||
|
||||
// SwiftUI
|
||||
Text("Tracked text")
|
||||
.tracking(2.0)
|
||||
.kerning(2.0) // Alternative API
|
||||
```
|
||||
|
||||
**Important:** Use `.tracking()` not `.kerning()` API for semantic correctness. Tracking disables ligatures when necessary; kerning does not.
|
||||
|
||||
### Leading (Line Spacing)
|
||||
|
||||
**Default Line Height:**
|
||||
Calculated from font's built-in metrics (ascender + descender + line gap).
|
||||
|
||||
**Language-Aware Adjustments:**
|
||||
iOS 17+ automatically increases line height for scripts with tall ascenders/descenders:
|
||||
- Arabic
|
||||
- Thai, Lao
|
||||
- Hindi, Bengali, Telugu
|
||||
|
||||
From WWDC 2023:
|
||||
> "Automatic line height adjustment for scripts with variable heights"
|
||||
|
||||
**Manual Leading:**
|
||||
```swift
|
||||
// UIKit
|
||||
let paragraphStyle = NSMutableParagraphStyle()
|
||||
paragraphStyle.lineSpacing = 8.0 // 8pt additional space
|
||||
|
||||
// SwiftUI (iOS 13+)
|
||||
Text("Custom spacing")
|
||||
.lineSpacing(8.0)
|
||||
```
|
||||
|
||||
**Line Height (iOS 26+):**
|
||||
|
||||
`.lineHeight()` sets baseline-to-baseline distance directly — more intuitive than `.lineSpacing()` (which measures bottom-to-top).
|
||||
|
||||
```swift
|
||||
// Presets
|
||||
Text("Open layout").lineHeight(.loose)
|
||||
Text("Compact layout").lineHeight(.tight)
|
||||
|
||||
// Precise control
|
||||
Text("Scaled").lineHeight(.multiple(factor: 1.5))
|
||||
Text("Fixed").lineHeight(.exact(points: 30)) // Does NOT scale with Dynamic Type
|
||||
```
|
||||
|
||||
Also available as `AttributedString.lineHeight` for styled strings. See `axiom-swiftui-26-ref` for full API details.
|
||||
|
||||
### Third-Party Font Tracking
|
||||
|
||||
**New in iOS 18:**
|
||||
Font vendors can embed tracking tables in custom fonts using STAT table + CTFont optical size attribute.
|
||||
|
||||
```swift
|
||||
let attributes: [String: Any] = [
|
||||
kCTFontOpticalSizeAttribute as String: pointSize
|
||||
]
|
||||
let descriptor = CTFontDescriptorCreateWithAttributes(attributes as CFDictionary)
|
||||
let font = CTFontCreateWithFontDescriptor(descriptor, pointSize, nil)
|
||||
```
|
||||
|
||||
## SwiftUI AttributedString Typography
|
||||
|
||||
### Font Environment Interaction
|
||||
|
||||
**Critical Pattern** When using `AttributedString` with SwiftUI's `Text`, paragraph styles (like `lineHeightMultiple`) can be lost if fonts come from the environment instead of the attributed content.
|
||||
|
||||
From WWDC 2025-280:
|
||||
> "TextEditor substitutes the default value calculated from the environment for any AttributedStringKeys with a value of nil."
|
||||
|
||||
This same principle applies to `Text`—when your `AttributedString` doesn't specify a font, SwiftUI applies the environment font, which can cause it to rebuild text runs and drop or normalize paragraph style details.
|
||||
|
||||
### The Problem
|
||||
|
||||
```swift
|
||||
// ❌ WRONG - .font() modifier can override and drop paragraph styles
|
||||
var s = AttributedString(longString)
|
||||
|
||||
// Set paragraph style
|
||||
var p = AttributedString.ParagraphStyle()
|
||||
p.lineHeightMultiple = 0.92
|
||||
s.paragraphStyle = p
|
||||
// ⚠️ No font set in AttributedString
|
||||
|
||||
Text(s)
|
||||
.font(.body) // ⚠️ May rebuild runs, lose lineHeightMultiple
|
||||
```
|
||||
|
||||
**Why this fails:**
|
||||
1. `AttributedString` has no font attribute set (value is `nil`)
|
||||
2. SwiftUI's `.font(.body)` modifier tells it "use this font for the whole run"
|
||||
3. SwiftUI rebuilds text runs with the environment font
|
||||
4. Paragraph styles get dropped or normalized during rebuild
|
||||
|
||||
### The Solution
|
||||
|
||||
**Keep typography inside the AttributedString when you need fine control:**
|
||||
|
||||
```swift
|
||||
// ✅ CORRECT - Font in AttributedString, no environment override
|
||||
var s = AttributedString(longString)
|
||||
|
||||
// Set font INSIDE the attributed content
|
||||
s.font = .system(.body) // ✅ Typography inside AttributedString
|
||||
|
||||
// Set paragraph style
|
||||
var p = AttributedString.ParagraphStyle()
|
||||
p.lineHeightMultiple = 0.92
|
||||
s.paragraphStyle = p
|
||||
|
||||
Text(s) // ✅ No .font() modifier
|
||||
```
|
||||
|
||||
**Why this works:**
|
||||
1. Font is part of the attributed content (not `nil`)
|
||||
2. No environment override from `.font()` modifier
|
||||
3. SwiftUI preserves both font AND paragraph styles
|
||||
4. Text runs remain intact with all attributes
|
||||
|
||||
### When to Use Each Approach
|
||||
|
||||
#### Use Font in AttributedString (Fine Control)
|
||||
|
||||
```swift
|
||||
var s = AttributedString("Carefully styled text")
|
||||
s.font = .system(.body)
|
||||
|
||||
var p = AttributedString.ParagraphStyle()
|
||||
p.lineHeightMultiple = 0.92
|
||||
p.alignment = .leading
|
||||
s.paragraphStyle = p
|
||||
|
||||
Text(s) // No modifier
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Need precise paragraph styling (line height, alignment)
|
||||
- Mixing multiple fonts in one string
|
||||
- Content will be displayed in both `Text` and `TextEditor`
|
||||
- Preserving exact formatting from rich text editor
|
||||
|
||||
#### Use .font() Modifier (Broad Override)
|
||||
|
||||
```swift
|
||||
Text("Simple text")
|
||||
.font(.body)
|
||||
.lineSpacing(4.0) // SwiftUI-level spacing
|
||||
```
|
||||
|
||||
**When to use:**
|
||||
- Simple text without paragraph styles
|
||||
- Want Dynamic Type automatic scaling
|
||||
- Need SwiftUI's semantic font behavior (Dark Mode, accessibility)
|
||||
- Intentionally overriding AttributedString fonts
|
||||
|
||||
### Multiple Fonts in One String
|
||||
|
||||
```swift
|
||||
var s = AttributedString("Title")
|
||||
s.font = .system(.title).bold()
|
||||
|
||||
var body = AttributedString(" and body text")
|
||||
body.font = .system(.body)
|
||||
|
||||
s.append(body)
|
||||
|
||||
Text(s) // ✅ No .font() modifier preserves both fonts
|
||||
```
|
||||
|
||||
### Common Mistake: Order Doesn't Matter
|
||||
|
||||
```swift
|
||||
// ❌ WRONG mental model: "Create AttributedString first"
|
||||
var s = AttributedString(text)
|
||||
var p = AttributedString.ParagraphStyle()
|
||||
p.lineHeightMultiple = 0.92
|
||||
s.paragraphStyle = p
|
||||
s.font = .system(.body) // ⚠️ Setting font last doesn't help if you use .font() modifier
|
||||
|
||||
Text(s).font(.body) // Still breaks!
|
||||
```
|
||||
|
||||
The issue isn't **when** you set the font in `AttributedString`. The issue is **whether the attributed content carries its own font attributes** versus relying on SwiftUI's `.font(...)` environment.
|
||||
|
||||
### Verification Checklist
|
||||
|
||||
When using `AttributedString` with paragraph styles:
|
||||
- [ ] Font set inside `AttributedString` (not `nil`)
|
||||
- [ ] No `.font()` modifier on `Text` view (unless intentionally overriding)
|
||||
- [ ] Paragraph styles set after or before font (order doesn't matter)
|
||||
- [ ] Tested with actual content to verify line height/alignment preserved
|
||||
|
||||
## Internationalization
|
||||
|
||||
### Bidirectional Text
|
||||
|
||||
**Complex Script Example (from WWDC 2021):**
|
||||
|
||||
Kannada word "October":
|
||||
- Character index 4 has split vowel → 2 glyphs
|
||||
- Glyphs reorder before ligature application
|
||||
- Glyph index ≠ character index
|
||||
|
||||
This is why TextKit 2 uses **NSTextLocation** instead of integer indices.
|
||||
|
||||
**Hebrew/Arabic Selection:**
|
||||
Single visual selection = multiple NSRanges in AttributedString due to right-to-left layout.
|
||||
|
||||
### Line Breaking
|
||||
|
||||
**Language-Aware (iOS 17+):**
|
||||
- Chinese, Japanese, Korean: break at semantic boundaries
|
||||
- German: avoid breaking compound words
|
||||
- English: prefer breaking at hyphens
|
||||
|
||||
**Even Line Breaking (TextKit 2):**
|
||||
Justified paragraphs use improved line breaking algorithm:
|
||||
- Reduces stretched-out lines
|
||||
- More even interword spacing
|
||||
- Automatic in TextKit 2
|
||||
|
||||
### Text Clipping Prevention
|
||||
|
||||
**Best Practices:**
|
||||
1. Use Dynamic Type (auto-adjusts)
|
||||
2. Set `.lineLimit(nil)` or `.lineLimit(2...5)` in SwiftUI
|
||||
3. Use `.minimumScaleFactor()` for constrained single-line text
|
||||
4. Test with large accessibility sizes
|
||||
|
||||
## CSS & Web Typography
|
||||
|
||||
**System UI Font Families:**
|
||||
|
||||
```css
|
||||
font-family: system-ui; /* SF Pro */
|
||||
font-family: ui-rounded; /* SF Pro Rounded */
|
||||
font-family: ui-serif; /* New York */
|
||||
font-family: ui-monospace; /* SF Mono */
|
||||
```
|
||||
|
||||
**Legacy:**
|
||||
```css
|
||||
font-family: -apple-system; /* deprecated, use system-ui */
|
||||
```
|
||||
|
||||
## Code Examples
|
||||
|
||||
### Emphasized Large Title (SwiftUI)
|
||||
```swift
|
||||
Text("Recipe Editor")
|
||||
.font(.largeTitle.bold()) // Emphasized variant
|
||||
```
|
||||
|
||||
### Custom Font + Dynamic Type (UIKit)
|
||||
```swift
|
||||
let customFont = UIFont(name: "Avenir-Medium", size: 17)!
|
||||
let metrics = UIFontMetrics(forTextStyle: .body)
|
||||
label.font = metrics.scaledFont(for: customFont)
|
||||
label.adjustsFontForContentSizeCategory = true
|
||||
```
|
||||
|
||||
### Rounded Design (UIKit)
|
||||
```swift
|
||||
let descriptor = UIFontDescriptor
|
||||
.preferredFontDescriptor(withTextStyle: .largeTitle)
|
||||
.withDesign(.rounded)!
|
||||
let font = UIFont(descriptor: descriptor, size: 0)
|
||||
```
|
||||
|
||||
### Rounded Design (SwiftUI)
|
||||
```swift
|
||||
Text("Today")
|
||||
.font(.largeTitle.bold())
|
||||
.fontDesign(.rounded)
|
||||
```
|
||||
|
||||
### ScaledMetric (SwiftUI)
|
||||
```swift
|
||||
struct RecipeView: View {
|
||||
@ScaledMetric(relativeTo: .body) var padding: CGFloat = 20
|
||||
|
||||
var body: some View {
|
||||
Text("Recipe")
|
||||
.padding(padding) // Scales with Dynamic Type
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Resources
|
||||
|
||||
**WWDC**: 2020-10175, 2022-110381, 2023-10058
|
||||
|
||||
**Docs**: /uikit/uifontdescriptor, /uikit/uifontmetrics, /swiftui/font
|
||||
3
.claude/skills/axiom-typography-ref/agents/openai.yaml
Normal file
3
.claude/skills/axiom-typography-ref/agents/openai.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
interface:
|
||||
display_name: "Typography Reference"
|
||||
short_description: "Apple platform typography reference (San Francisco fonts, text styles, Dynamic Type, tracking, leading, international..."
|
||||
Reference in New Issue
Block a user