claudecode-omc 5.6.6 → 5.6.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
- package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
- package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
- package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
- package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
- package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
- package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
- package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
- package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
- package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
- package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
- package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
- package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
- package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
- package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
- package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
- package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
- package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
- package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
- package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
- package/bundled/manifest.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
// CalibrationScreen.swift
|
|
2
|
+
// Calibration Twin — SwiftUI side of the Stage 2.5 render-equivalence pair.
|
|
3
|
+
//
|
|
4
|
+
// IMPORTANT: every color, size, and spacing constant here derives from the
|
|
5
|
+
// same design-token values in tokens.json and h5-twin/style.css.
|
|
6
|
+
// Do NOT change any value without updating all three files.
|
|
7
|
+
//
|
|
8
|
+
// Layout target: iPhone 15 Pro logical viewport 393×852 pt,
|
|
9
|
+
// with a 59 pt top safe-area inset equivalent.
|
|
10
|
+
|
|
11
|
+
import SwiftUI
|
|
12
|
+
|
|
13
|
+
// ── Design Tokens ────────────────────────────────────────────────────────── //
|
|
14
|
+
// Hex → Color(red:green:blue:) conversions use the formula r/255, g/255, b/255
|
|
15
|
+
// with sRGB color space (matching the CSS hex values in style.css).
|
|
16
|
+
|
|
17
|
+
private enum Tokens {
|
|
18
|
+
// Colors
|
|
19
|
+
// #F2F2F7 → r=0xF2/255=0.9490 g=0xF2/255=0.9490 b=0xF7/255=0.9686
|
|
20
|
+
static let colorBackground = Color(red: 0xF2/255.0, green: 0xF2/255.0, blue: 0xF7/255.0)
|
|
21
|
+
// #4A90D9 → r=0x4A/255=0.2902 g=0x90/255=0.5647 b=0xD9/255=0.8510
|
|
22
|
+
static let colorCard = Color(red: 0x4A/255.0, green: 0x90/255.0, blue: 0xD9/255.0)
|
|
23
|
+
// #E8744F → r=0xE8/255=0.9098 g=0x74/255=0.4549 b=0x4F/255=0.3098
|
|
24
|
+
static let colorCircle = Color(red: 0xE8/255.0, green: 0x74/255.0, blue: 0x4F/255.0)
|
|
25
|
+
|
|
26
|
+
// Card stripes — structured multi-color region (NOT a flat block) so the
|
|
27
|
+
// measured SSIM floor is meaningful.
|
|
28
|
+
// #4A90D9 blue / #7E57C2 purple / #26A69A teal / #F4B400 amber
|
|
29
|
+
static let colorStripeA = Color(red: 0x4A/255.0, green: 0x90/255.0, blue: 0xD9/255.0)
|
|
30
|
+
static let colorStripeB = Color(red: 0x7E/255.0, green: 0x57/255.0, blue: 0xC2/255.0)
|
|
31
|
+
static let colorStripeC = Color(red: 0x26/255.0, green: 0xA6/255.0, blue: 0x9A/255.0)
|
|
32
|
+
static let colorStripeD = Color(red: 0xF4/255.0, green: 0xB4/255.0, blue: 0x00/255.0)
|
|
33
|
+
// #1C1C1E → r=0x1C/255=0.1098 g=0x1C/255=0.1098 b=0x1E/255=0.1176
|
|
34
|
+
static let colorTextHeading = Color(red: 0x1C/255.0, green: 0x1C/255.0, blue: 0x1E/255.0)
|
|
35
|
+
// #636366 → r=0x63/255=0.3882 g=0x63/255=0.3882 b=0x66/255=0.4000
|
|
36
|
+
static let colorTextBody = Color(red: 0x63/255.0, green: 0x63/255.0, blue: 0x66/255.0)
|
|
37
|
+
|
|
38
|
+
// Spacing (pt — 1 pt = 1 CSS px at 1× logical scale)
|
|
39
|
+
static let spaceSafeTop: CGFloat = 59 // top safe-area equivalent
|
|
40
|
+
static let spacePageH: CGFloat = 20 // horizontal page padding
|
|
41
|
+
static let spaceSectionGap: CGFloat = 24 // gap: text block → shape block
|
|
42
|
+
static let spaceCardPadding: CGFloat = 20 // inner card padding
|
|
43
|
+
static let spaceCardGap: CGFloat = 16 // gap: card → circle row
|
|
44
|
+
|
|
45
|
+
// Radii
|
|
46
|
+
static let radiusCard: CGFloat = 16
|
|
47
|
+
|
|
48
|
+
// Typography (SF System font — closest match to -apple-system CSS)
|
|
49
|
+
static let fontSizeHeading: CGFloat = 22
|
|
50
|
+
static let fontWeightHeading: Font.Weight = .semibold // CSS 600
|
|
51
|
+
static let fontSizeBody: CGFloat = 15
|
|
52
|
+
static let fontWeightBody: Font.Weight = .regular // CSS 400
|
|
53
|
+
|
|
54
|
+
// Shape
|
|
55
|
+
static let sizeCircle: CGFloat = 64
|
|
56
|
+
|
|
57
|
+
// Screen frame (iPhone 15 Pro logical size)
|
|
58
|
+
static let screenWidth: CGFloat = 393
|
|
59
|
+
static let screenHeight: CGFloat = 852
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// ── CalibrationScreenView ─────────────────────────────────────────────────── //
|
|
63
|
+
|
|
64
|
+
public struct CalibrationScreenView: View {
|
|
65
|
+
|
|
66
|
+
public init() {}
|
|
67
|
+
|
|
68
|
+
public var body: some View {
|
|
69
|
+
ZStack(alignment: .topLeading) {
|
|
70
|
+
// Background fills the entire 393×852 frame
|
|
71
|
+
Tokens.colorBackground
|
|
72
|
+
.ignoresSafeArea()
|
|
73
|
+
|
|
74
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
75
|
+
|
|
76
|
+
// Safe-area top inset (59 pt) — mirrors --space-safe-top in CSS
|
|
77
|
+
Spacer()
|
|
78
|
+
.frame(height: Tokens.spaceSafeTop)
|
|
79
|
+
|
|
80
|
+
// ── Text Block ──────────────────────────────────────────── //
|
|
81
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
82
|
+
// Heading: 22pt semibold, color #1C1C1E
|
|
83
|
+
Text("Calibration Screen")
|
|
84
|
+
.font(.system(size: Tokens.fontSizeHeading,
|
|
85
|
+
weight: Tokens.fontWeightHeading))
|
|
86
|
+
.foregroundColor(Tokens.colorTextHeading)
|
|
87
|
+
.lineSpacing(Tokens.fontSizeHeading * (1.27 - 1))
|
|
88
|
+
// line-height ≈ 1.27 × 22 ≈ 28 pt — matches CSS line-height
|
|
89
|
+
|
|
90
|
+
// Body: 15pt regular, color #636366
|
|
91
|
+
Text("This screen is the known-correct SwiftUI twin used to measure the cross-renderer fidelity floor. It is not a conversion target.")
|
|
92
|
+
.font(.system(size: Tokens.fontSizeBody,
|
|
93
|
+
weight: Tokens.fontWeightBody))
|
|
94
|
+
.foregroundColor(Tokens.colorTextBody)
|
|
95
|
+
.lineSpacing(Tokens.fontSizeBody * (1.47 - 1))
|
|
96
|
+
// line-height ≈ 1.47 × 15 ≈ 22 pt — matches CSS line-height
|
|
97
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
98
|
+
}
|
|
99
|
+
.padding(.horizontal, Tokens.spacePageH)
|
|
100
|
+
|
|
101
|
+
// Gap between text block and shape block (24 pt)
|
|
102
|
+
Spacer()
|
|
103
|
+
.frame(height: Tokens.spaceSectionGap)
|
|
104
|
+
|
|
105
|
+
// ── Shape Block ─────────────────────────────────────────── //
|
|
106
|
+
VStack(alignment: .leading, spacing: Tokens.spaceCardGap) {
|
|
107
|
+
|
|
108
|
+
// Structured (4-stripe) rounded-rect card, radius 16pt.
|
|
109
|
+
// Deliberately NOT a flat solid block — a flat region makes
|
|
110
|
+
// SSIM blind to a uniform mean shift, so the measured floor
|
|
111
|
+
// would be unreliable. Height = 2 × card padding = 40pt
|
|
112
|
+
// (same overall card height as the original solid block).
|
|
113
|
+
HStack(spacing: 0) {
|
|
114
|
+
Tokens.colorStripeA
|
|
115
|
+
Tokens.colorStripeB
|
|
116
|
+
Tokens.colorStripeC
|
|
117
|
+
Tokens.colorStripeD
|
|
118
|
+
}
|
|
119
|
+
.frame(height: Tokens.spaceCardPadding * 2)
|
|
120
|
+
.clipShape(RoundedRectangle(cornerRadius: Tokens.radiusCard))
|
|
121
|
+
// width = full content width (set by horizontal padding below)
|
|
122
|
+
|
|
123
|
+
// Circle (#E8744F, 64×64 pt)
|
|
124
|
+
Circle()
|
|
125
|
+
.fill(Tokens.colorCircle)
|
|
126
|
+
.frame(width: Tokens.sizeCircle, height: Tokens.sizeCircle)
|
|
127
|
+
}
|
|
128
|
+
.padding(.horizontal, Tokens.spacePageH)
|
|
129
|
+
|
|
130
|
+
Spacer()
|
|
131
|
+
}
|
|
132
|
+
.frame(width: Tokens.screenWidth, alignment: .topLeading)
|
|
133
|
+
}
|
|
134
|
+
.frame(width: Tokens.screenWidth, height: Tokens.screenHeight)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Preview ──────────────────────────────────────────────────────────────── //
|
|
139
|
+
|
|
140
|
+
#Preview("Calibration Screen — 393×852") {
|
|
141
|
+
CalibrationScreenView()
|
|
142
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
// swift-tools-version: 5.9
|
|
2
|
+
// Calibration DIVERGENT Twin — SwiftUI Package (NEGATIVE CONTROL)
|
|
3
|
+
//
|
|
4
|
+
// This package is the deliberately-WRONG SwiftUI screen used as the Stage 5
|
|
5
|
+
// judge negative control. The independent judge MUST reject this vs the H5
|
|
6
|
+
// twin; if it does not, its YES is voided for that run (see
|
|
7
|
+
// references/visual-diff-loop-protocol.md and evaluate-convergence.mjs).
|
|
8
|
+
//
|
|
9
|
+
// Zero third-party dependencies. Requires Xcode 15+ / Swift 5.9+.
|
|
10
|
+
|
|
11
|
+
import PackageDescription
|
|
12
|
+
|
|
13
|
+
let package = Package(
|
|
14
|
+
name: "CalibrationScreenDivergent",
|
|
15
|
+
platforms: [
|
|
16
|
+
.iOS(.v17),
|
|
17
|
+
.macOS(.v14)
|
|
18
|
+
],
|
|
19
|
+
products: [
|
|
20
|
+
.library(
|
|
21
|
+
name: "CalibrationScreenDivergent",
|
|
22
|
+
targets: ["CalibrationScreenDivergent"]
|
|
23
|
+
)
|
|
24
|
+
],
|
|
25
|
+
targets: [
|
|
26
|
+
.target(
|
|
27
|
+
name: "CalibrationScreenDivergent",
|
|
28
|
+
dependencies: [],
|
|
29
|
+
path: "Sources/CalibrationScreenDivergent"
|
|
30
|
+
)
|
|
31
|
+
]
|
|
32
|
+
)
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
// CalibrationScreenDivergent.swift
|
|
2
|
+
// NEGATIVE CONTROL — the deliberately-WRONG SwiftUI side of the Stage 5
|
|
3
|
+
// judge negative control.
|
|
4
|
+
//
|
|
5
|
+
// This screen is INTENTIONALLY divergent from the H5 twin
|
|
6
|
+
// (assets/calibration/h5-twin/index.html):
|
|
7
|
+
// • WRONG colors — background green not grey, card stripes recolored,
|
|
8
|
+
// circle blue not coral, text colors inverted.
|
|
9
|
+
// • SHIFTED layout — larger top inset, different padding, the circle is
|
|
10
|
+
// moved to the trailing edge, the card is taller and
|
|
11
|
+
// narrower, section gap doubled.
|
|
12
|
+
// • WRONG copy — heading/body text differs.
|
|
13
|
+
//
|
|
14
|
+
// The independent judge MUST reject this pair (H5 twin vs this) under the
|
|
15
|
+
// adversarial forced-difference-3 framing. If the judge fails to reject it,
|
|
16
|
+
// `judge.negative_control` is recorded as "failed" and any YES verdict is
|
|
17
|
+
// VOID for that run (enforced mechanically by evaluate-convergence.mjs).
|
|
18
|
+
//
|
|
19
|
+
// It is still TEXTURED (text + a multi-color stripe region) so the contrast
|
|
20
|
+
// with the correct twin is structural, not just a flat mean shift.
|
|
21
|
+
|
|
22
|
+
import SwiftUI
|
|
23
|
+
|
|
24
|
+
private enum WrongTokens {
|
|
25
|
+
// WRONG colors (intentionally divergent from tokens.json)
|
|
26
|
+
static let colorBackground = Color(red: 0x2E/255.0, green: 0x7D/255.0, blue: 0x32/255.0) // green, should be #F2F2F7
|
|
27
|
+
static let colorCircle = Color(red: 0x15/255.0, green: 0x65/255.0, blue: 0xC0/255.0) // blue, should be #E8744F
|
|
28
|
+
static let colorTextHeading = Color(red: 0xFF/255.0, green: 0xFF/255.0, blue: 0xFF/255.0) // white, should be #1C1C1E
|
|
29
|
+
static let colorTextBody = Color(red: 0xFF/255.0, green: 0xEB/255.0, blue: 0x3B/255.0) // yellow, should be #636366
|
|
30
|
+
|
|
31
|
+
// WRONG stripe colors (recolored + reordered)
|
|
32
|
+
static let colorStripeA = Color(red: 0xD3/255.0, green: 0x2F/255.0, blue: 0x2F/255.0) // red
|
|
33
|
+
static let colorStripeB = Color(red: 0xFB/255.0, green: 0x8C/255.0, blue: 0x00/255.0) // orange
|
|
34
|
+
static let colorStripeC = Color(red: 0x00/255.0, green: 0x00/255.0, blue: 0x00/255.0) // black
|
|
35
|
+
static let colorStripeD = Color(red: 0xFF/255.0, green: 0xFF/255.0, blue: 0xFF/255.0) // white
|
|
36
|
+
|
|
37
|
+
// SHIFTED layout (intentionally divergent spacing)
|
|
38
|
+
static let spaceSafeTop: CGFloat = 140 // should be 59 — large shift
|
|
39
|
+
static let spacePageH: CGFloat = 48 // should be 20
|
|
40
|
+
static let spaceSectionGap: CGFloat = 48 // should be 24 — doubled
|
|
41
|
+
static let spaceCardPadding: CGFloat = 20
|
|
42
|
+
static let spaceCardGap: CGFloat = 16
|
|
43
|
+
|
|
44
|
+
static let radiusCard: CGFloat = 2 // should be 16 — nearly square
|
|
45
|
+
static let fontSizeHeading: CGFloat = 34 // should be 22
|
|
46
|
+
static let fontSizeBody: CGFloat = 11 // should be 15
|
|
47
|
+
static let sizeCircle: CGFloat = 110 // should be 64 — much larger
|
|
48
|
+
|
|
49
|
+
static let screenWidth: CGFloat = 393
|
|
50
|
+
static let screenHeight: CGFloat = 852
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// ── CalibrationScreenDivergentView ────────────────────────────────────────── //
|
|
54
|
+
|
|
55
|
+
public struct CalibrationScreenDivergentView: View {
|
|
56
|
+
|
|
57
|
+
public init() {}
|
|
58
|
+
|
|
59
|
+
public var body: some View {
|
|
60
|
+
ZStack(alignment: .topLeading) {
|
|
61
|
+
WrongTokens.colorBackground
|
|
62
|
+
.ignoresSafeArea()
|
|
63
|
+
|
|
64
|
+
VStack(alignment: .leading, spacing: 0) {
|
|
65
|
+
|
|
66
|
+
Spacer()
|
|
67
|
+
.frame(height: WrongTokens.spaceSafeTop)
|
|
68
|
+
|
|
69
|
+
// WRONG copy + WRONG type sizes + WRONG colors
|
|
70
|
+
VStack(alignment: .leading, spacing: 8) {
|
|
71
|
+
Text("WRONG SCREEN — negative control")
|
|
72
|
+
.font(.system(size: WrongTokens.fontSizeHeading,
|
|
73
|
+
weight: .bold))
|
|
74
|
+
.foregroundColor(WrongTokens.colorTextHeading)
|
|
75
|
+
|
|
76
|
+
Text("If a judge calls this equivalent to the H5 twin, its verdict is void.")
|
|
77
|
+
.font(.system(size: WrongTokens.fontSizeBody,
|
|
78
|
+
weight: .regular))
|
|
79
|
+
.foregroundColor(WrongTokens.colorTextBody)
|
|
80
|
+
.fixedSize(horizontal: false, vertical: true)
|
|
81
|
+
}
|
|
82
|
+
.padding(.horizontal, WrongTokens.spacePageH)
|
|
83
|
+
|
|
84
|
+
Spacer()
|
|
85
|
+
.frame(height: WrongTokens.spaceSectionGap)
|
|
86
|
+
|
|
87
|
+
VStack(alignment: .leading, spacing: WrongTokens.spaceCardGap) {
|
|
88
|
+
|
|
89
|
+
// Taller, near-square, recolored stripe card
|
|
90
|
+
HStack(spacing: 0) {
|
|
91
|
+
WrongTokens.colorStripeA
|
|
92
|
+
WrongTokens.colorStripeB
|
|
93
|
+
WrongTokens.colorStripeC
|
|
94
|
+
WrongTokens.colorStripeD
|
|
95
|
+
}
|
|
96
|
+
.frame(height: WrongTokens.spaceCardPadding * 5) // much taller
|
|
97
|
+
.clipShape(RoundedRectangle(cornerRadius: WrongTokens.radiusCard))
|
|
98
|
+
|
|
99
|
+
// Circle moved to the TRAILING edge, wrong color, much bigger
|
|
100
|
+
HStack {
|
|
101
|
+
Spacer()
|
|
102
|
+
Circle()
|
|
103
|
+
.fill(WrongTokens.colorCircle)
|
|
104
|
+
.frame(width: WrongTokens.sizeCircle,
|
|
105
|
+
height: WrongTokens.sizeCircle)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
.padding(.horizontal, WrongTokens.spacePageH)
|
|
109
|
+
|
|
110
|
+
Spacer()
|
|
111
|
+
}
|
|
112
|
+
.frame(width: WrongTokens.screenWidth, alignment: .topLeading)
|
|
113
|
+
}
|
|
114
|
+
.frame(width: WrongTokens.screenWidth, height: WrongTokens.screenHeight)
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// ── Preview ──────────────────────────────────────────────────────────────── //
|
|
119
|
+
|
|
120
|
+
#Preview("DIVERGENT (negative control) — 393×852") {
|
|
121
|
+
CalibrationScreenDivergentView()
|
|
122
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "h5-to-swiftui/calibration-tokens@1",
|
|
3
|
+
"$comment": "Ground-truth design tokens for the Stage 2.5 calibration pair. Both h5-twin/style.css and swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift MUST implement every value here exactly. Any measured rendering difference between the two sides is the irreducible cross-renderer floor.",
|
|
4
|
+
|
|
5
|
+
"color": {
|
|
6
|
+
"background": { "$value": "#F2F2F7", "$type": "color", "$comment": "iOS systemGroupedBackground equivalent" },
|
|
7
|
+
"card": { "$value": "#4A90D9", "$type": "color", "$comment": "blue card base; carries multi-color stripes (textured, not flat)" },
|
|
8
|
+
"circle": { "$value": "#E8744F", "$type": "color", "$comment": "coral orange circle" },
|
|
9
|
+
"textHeading": { "$value": "#1C1C1E", "$type": "color", "$comment": "iOS label equivalent" },
|
|
10
|
+
"textBody": { "$value": "#636366", "$type": "color", "$comment": "iOS secondaryLabel equivalent" },
|
|
11
|
+
"stripeA": { "$value": "#4A90D9", "$type": "color", "$comment": "card stripe 1 (blue) — structured region so SSIM is meaningful" },
|
|
12
|
+
"stripeB": { "$value": "#7E57C2", "$type": "color", "$comment": "card stripe 2 (purple)" },
|
|
13
|
+
"stripeC": { "$value": "#26A69A", "$type": "color", "$comment": "card stripe 3 (teal)" },
|
|
14
|
+
"stripeD": { "$value": "#F4B400", "$type": "color", "$comment": "card stripe 4 (amber)" }
|
|
15
|
+
},
|
|
16
|
+
|
|
17
|
+
"space": {
|
|
18
|
+
"safeTop": { "$value": 59, "$unit": "px_pt", "$comment": "iPhone 15 Pro top safe-area inset, CSS px = Swift pt at 1× logical scale" },
|
|
19
|
+
"pageH": { "$value": 20, "$unit": "px_pt", "$comment": "horizontal page padding" },
|
|
20
|
+
"sectionGap": { "$value": 24, "$unit": "px_pt", "$comment": "vertical gap between text block and shape block" },
|
|
21
|
+
"cardPadding": { "$value": 20, "$unit": "px_pt", "$comment": "inner padding of the blue card" },
|
|
22
|
+
"cardGap": { "$value": 16, "$unit": "px_pt", "$comment": "gap between card and circle row" }
|
|
23
|
+
},
|
|
24
|
+
|
|
25
|
+
"radius": {
|
|
26
|
+
"card": { "$value": 16, "$unit": "px_pt", "$comment": "border-radius of the blue card" }
|
|
27
|
+
},
|
|
28
|
+
|
|
29
|
+
"font": {
|
|
30
|
+
"family": { "$value": "-apple-system", "$type": "fontFamily", "$comment": "CSS: -apple-system / system-ui; Swift: .system(size:weight:)" },
|
|
31
|
+
"sizeHeading": { "$value": 22, "$unit": "px_pt" },
|
|
32
|
+
"weightHeading": { "$value": 600, "$comment": "CSS 600 = semibold; Swift .semibold" },
|
|
33
|
+
"lineHeightHeading":{ "$value": 1.27, "$unit": "multiplier", "$comment": "unitless multiplier; 22 × 1.27 ≈ 28 pt" },
|
|
34
|
+
"sizeBody": { "$value": 15, "$unit": "px_pt" },
|
|
35
|
+
"weightBody": { "$value": 400, "$comment": "CSS 400 = regular; Swift .regular" },
|
|
36
|
+
"lineHeightBody": { "$value": 1.47, "$unit": "multiplier", "$comment": "unitless multiplier; 15 × 1.47 ≈ 22 pt" }
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
"shape": {
|
|
40
|
+
"circleSize": { "$value": 64, "$unit": "px_pt", "$comment": "diameter of the coral circle" }
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="UTF-8" />
|
|
5
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
6
|
+
<title>Chronicle Feed</title>
|
|
7
|
+
<link rel="stylesheet" href="/src/styles/tokens.css" />
|
|
8
|
+
<link rel="stylesheet" href="/src/styles/global.css" />
|
|
9
|
+
</head>
|
|
10
|
+
<body>
|
|
11
|
+
<div id="root"></div>
|
|
12
|
+
<script type="module" src="/src/main.jsx"></script>
|
|
13
|
+
</body>
|
|
14
|
+
</html>
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "chronicle-feed",
|
|
3
|
+
"version": "1.0.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"type": "module",
|
|
6
|
+
"scripts": {
|
|
7
|
+
"dev": "vite",
|
|
8
|
+
"build": "vite build",
|
|
9
|
+
"preview": "vite preview"
|
|
10
|
+
},
|
|
11
|
+
"dependencies": {
|
|
12
|
+
"react": "^18.3.1",
|
|
13
|
+
"react-dom": "^18.3.1",
|
|
14
|
+
"react-router-dom": "^6.26.1"
|
|
15
|
+
},
|
|
16
|
+
"devDependencies": {
|
|
17
|
+
"@vitejs/plugin-react": "^4.3.1",
|
|
18
|
+
"vite": "^5.4.2"
|
|
19
|
+
}
|
|
20
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "001",
|
|
3
|
+
"title": "SwiftUI Custom Layouts: A Complete Guide",
|
|
4
|
+
"subtitle": "How the Layout protocol changes everything you thought you knew about view composition",
|
|
5
|
+
"tag": "SwiftUI",
|
|
6
|
+
"author": {
|
|
7
|
+
"name": "Elena Vasquez",
|
|
8
|
+
"role": "Senior iOS Engineer",
|
|
9
|
+
"bio": "Elena writes about Swift, SwiftUI, and the craft of building beautiful iOS apps. Previously at Apple UIKit team."
|
|
10
|
+
},
|
|
11
|
+
"publishedAt": "2026-05-12T08:00:00Z",
|
|
12
|
+
"readingTime": 9,
|
|
13
|
+
"body": [
|
|
14
|
+
{
|
|
15
|
+
"type": "paragraph",
|
|
16
|
+
"text": "Before iOS 16, building a tag cloud in SwiftUI required creative workarounds: measuring text with a hidden Text view, storing widths in a preference key, and recomputing rows in a second pass. The result was fragile, verbose, and broke Dynamic Type. The Layout protocol changes all of that."
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"type": "heading",
|
|
20
|
+
"text": "What the Layout Protocol Actually Does"
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"type": "paragraph",
|
|
24
|
+
"text": "The Layout protocol exposes two methods: sizeThatFits and placeSubviews. Together they replicate the two-pass layout model that UIKit's layoutSubviews always had, but in a pure-Swift, declarative API that composes naturally with other SwiftUI views."
|
|
25
|
+
},
|
|
26
|
+
{
|
|
27
|
+
"type": "paragraph",
|
|
28
|
+
"text": "The key insight is that a Layout conformer does not manage a view hierarchy — it manages geometry. Your subviews remain ordinary SwiftUI views with all their modifiers and state intact. The Layout only decides where they go and how much space they claim."
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"type": "pullquote",
|
|
32
|
+
"text": "A Layout conformer does not manage a view hierarchy. It manages geometry. The distinction matters more than it seems.",
|
|
33
|
+
"attribution": "Apple WWDC22-10056, Compose custom layouts with SwiftUI"
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
"type": "heading",
|
|
37
|
+
"text": "The FlowLayout Pattern"
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
"type": "paragraph",
|
|
41
|
+
"text": "The most common use case is a flow layout — the direct equivalent of CSS flex-wrap: wrap. Items line up horizontally until they would overflow, then wrap to the next row. Each row height is determined by the tallest item in that row."
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
"type": "subheading",
|
|
45
|
+
"text": "sizeThatFits: Measure First"
|
|
46
|
+
},
|
|
47
|
+
{
|
|
48
|
+
"type": "paragraph",
|
|
49
|
+
"text": "The sizeThatFits pass walks through subviews, asking each one for its ideal size via sizeThatFits(.unspecified). It tracks the current x offset and wraps to a new row whenever adding the next item would exceed the proposed width. The total height is the sum of all row heights plus spacing between rows."
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
"type": "code",
|
|
53
|
+
"caption": "FlowLayout.swift — sizeThatFits",
|
|
54
|
+
"text": "func sizeThatFits(\n proposal: ProposedViewSize,\n subviews: Subviews,\n cache: inout ()\n) -> CGSize {\n let maxWidth = proposal.width ?? .infinity\n var x: CGFloat = 0\n var y: CGFloat = 0\n var rowHeight: CGFloat = 0\n\n for view in subviews {\n let size = view.sizeThatFits(.unspecified)\n if x + size.width > maxWidth, x > 0 {\n y += rowHeight + spacing\n x = 0\n rowHeight = 0\n }\n x += size.width + spacing\n rowHeight = max(rowHeight, size.height)\n }\n return CGSize(width: maxWidth, height: y + rowHeight)\n}"
|
|
55
|
+
},
|
|
56
|
+
{
|
|
57
|
+
"type": "subheading",
|
|
58
|
+
"text": "placeSubviews: Commit the Geometry"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"type": "paragraph",
|
|
62
|
+
"text": "The placeSubviews pass repeats the same walk — you cannot cache positions between the two passes in the void-cache form — and calls place(at:proposal:) on each subview. The coordinates are in the container's local coordinate space, with origin at the top-left of the bounds rect."
|
|
63
|
+
},
|
|
64
|
+
{
|
|
65
|
+
"type": "caption",
|
|
66
|
+
"text": "Note: add a struct Cache type to avoid measuring subviews twice when measurement is expensive."
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
"type": "divider"
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
"type": "heading",
|
|
73
|
+
"text": "When to Reach for Custom Layout"
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
"type": "paragraph",
|
|
77
|
+
"text": "Not every complex layout needs a custom Layout conformer. LazyVGrid with GridItem(.adaptive(minimum:)) handles the vast majority of responsive card grids. HStack and VStack with Spacer handle nearly all flex-direction use cases. Reserve custom Layout for four specific patterns: flex-wrap, non-uniform flex-grow ratios, non-uniform fr ratios, and radial placement."
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
"type": "paragraph",
|
|
81
|
+
"text": "The cost of a custom Layout is measured in code, not performance. The layout engine is highly optimized — a properly implemented FlowLayout is not measurably slower than LazyVGrid. The cost is the additional code surface to maintain and test. Write it once, test it, package it, and reuse it."
|
|
82
|
+
},
|
|
83
|
+
{
|
|
84
|
+
"type": "heading",
|
|
85
|
+
"text": "Dynamic Type and the Layout Protocol"
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
"type": "paragraph",
|
|
89
|
+
"text": "One of the best properties of a Layout-based flow is that it handles Dynamic Type correctly with zero extra work. Because each subview measures itself at its natural size — including any text scaling from the environment — the flow layout automatically reflows items when the user changes their preferred text size. Compare this to a hardcoded UICollectionViewFlowLayout with itemSize: there is no equivalent automatic reflow."
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
"type": "paragraph",
|
|
93
|
+
"text": "This composability — layout logic fully decoupled from content — is the deepest benefit of the Layout protocol. Build the geometry algorithm once. Apply it to any content. The content takes care of itself."
|
|
94
|
+
}
|
|
95
|
+
]
|
|
96
|
+
}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
{
|
|
2
|
+
"articles": [
|
|
3
|
+
{
|
|
4
|
+
"id": "001",
|
|
5
|
+
"title": "SwiftUI Custom Layouts: A Complete Guide",
|
|
6
|
+
"subtitle": "How the Layout protocol changes everything you thought you knew about view composition",
|
|
7
|
+
"summary": "Apple's Layout protocol, introduced in iOS 16, fills the last major gap between CSS Flexbox and SwiftUI. Here is how to use it without rewriting your entire view hierarchy.",
|
|
8
|
+
"tag": "SwiftUI",
|
|
9
|
+
"author": {
|
|
10
|
+
"name": "Elena Vasquez",
|
|
11
|
+
"role": "Senior iOS Engineer",
|
|
12
|
+
"bio": "Elena writes about Swift, SwiftUI, and the craft of building beautiful iOS apps. Previously at Apple UIKit team."
|
|
13
|
+
},
|
|
14
|
+
"publishedAt": "2026-05-12T08:00:00Z",
|
|
15
|
+
"readingTime": 9,
|
|
16
|
+
"imageAlt": "Diagram showing SwiftUI layout protocol flow"
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
"id": "002",
|
|
20
|
+
"title": "CSS-to-SwiftUI: The Mapping Table You Actually Need",
|
|
21
|
+
"subtitle": "Every flex and grid property mapped — with the caveats that break your layout",
|
|
22
|
+
"summary": "Mechanical transpilation from CSS to SwiftUI fails at six structural boundaries. This guide documents every mapping that matters for pixel-level parity and the exact divergences you need to handle manually.",
|
|
23
|
+
"tag": "Design",
|
|
24
|
+
"author": {
|
|
25
|
+
"name": "Marcus Liu",
|
|
26
|
+
"role": "Design Engineer"
|
|
27
|
+
},
|
|
28
|
+
"publishedAt": "2026-05-08T10:30:00Z",
|
|
29
|
+
"readingTime": 12,
|
|
30
|
+
"imageAlt": "Side-by-side CSS and SwiftUI code comparison"
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
"id": "003",
|
|
34
|
+
"title": "WebGL on iOS: When to Keep It and When to Rewrite",
|
|
35
|
+
"subtitle": "A practical triage guide for GPU-heavy web content in a native app migration",
|
|
36
|
+
"summary": "Not every canvas element should become a Metal view. Understanding the performance ceiling and fidelity trade-offs helps you make the right call on each surface — and avoid shipping a half-native app.",
|
|
37
|
+
"tag": "Engineering",
|
|
38
|
+
"author": {
|
|
39
|
+
"name": "Priya Nair",
|
|
40
|
+
"role": "Platform Engineer"
|
|
41
|
+
},
|
|
42
|
+
"publishedAt": "2026-05-01T09:15:00Z",
|
|
43
|
+
"readingTime": 7,
|
|
44
|
+
"imageAlt": "GPU pipeline diagram"
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
"id": "004",
|
|
48
|
+
"title": "Typography at the iOS Scale: CoreText vs WebKit",
|
|
49
|
+
"subtitle": "Why identical font declarations produce different layouts — and how to fix them",
|
|
50
|
+
"summary": "line-height: 1.5 means something different in every rendering engine. This deep-dive explains WebKit's half-leading, CoreText's font metrics, and the exact formula to convert between them without visual drift.",
|
|
51
|
+
"tag": "Design",
|
|
52
|
+
"author": {
|
|
53
|
+
"name": "Sofia Andersson",
|
|
54
|
+
"role": "Type Systems Lead"
|
|
55
|
+
},
|
|
56
|
+
"publishedAt": "2026-04-28T07:45:00Z",
|
|
57
|
+
"readingTime": 11,
|
|
58
|
+
"imageAlt": "Typography baseline grid comparison"
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
"id": "005",
|
|
62
|
+
"title": "Accessibility Tokens: Building a Design System for Both Web and iOS",
|
|
63
|
+
"subtitle": "One token source of truth, two rendering targets, zero compromises",
|
|
64
|
+
"summary": "Design tokens are the bridge between your CSS custom properties and SwiftUI's Color and Font extensions. When you get the extraction pipeline right, dark mode and Dynamic Type come for free on both platforms.",
|
|
65
|
+
"tag": "Accessibility",
|
|
66
|
+
"author": {
|
|
67
|
+
"name": "Elena Vasquez",
|
|
68
|
+
"role": "Senior iOS Engineer"
|
|
69
|
+
},
|
|
70
|
+
"publishedAt": "2026-04-20T11:00:00Z",
|
|
71
|
+
"readingTime": 8,
|
|
72
|
+
"imageAlt": "Design token pipeline diagram"
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
"id": "006",
|
|
76
|
+
"title": "Machine Learning at the Edge: Core ML vs WebAssembly",
|
|
77
|
+
"subtitle": "How on-device inference differs between iOS and modern browsers",
|
|
78
|
+
"summary": "WebAssembly SIMD can achieve near-native throughput for small models, but Core ML's neural engine offers a 10x power efficiency advantage. Here is when each approach wins.",
|
|
79
|
+
"tag": "Machine Learning",
|
|
80
|
+
"author": {
|
|
81
|
+
"name": "James Park",
|
|
82
|
+
"role": "ML Platform Lead"
|
|
83
|
+
},
|
|
84
|
+
"publishedAt": "2026-04-15T14:00:00Z",
|
|
85
|
+
"readingTime": 14,
|
|
86
|
+
"imageAlt": "Core ML vs WASM inference benchmark chart"
|
|
87
|
+
}
|
|
88
|
+
]
|
|
89
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import React, { useState } from 'react';
|
|
2
|
+
import { Routes, Route, useNavigate } from 'react-router-dom';
|
|
3
|
+
import FeedScreen from './screens/FeedScreen.jsx';
|
|
4
|
+
import ArticleScreen from './screens/ArticleScreen.jsx';
|
|
5
|
+
import NavBar from './components/NavBar.jsx';
|
|
6
|
+
import styles from './App.module.css';
|
|
7
|
+
|
|
8
|
+
export default function App() {
|
|
9
|
+
const [activeTab, setActiveTab] = useState('feed');
|
|
10
|
+
|
|
11
|
+
return (
|
|
12
|
+
<div className={styles.appRoot}>
|
|
13
|
+
<NavBar activeTab={activeTab} onTabChange={setActiveTab} />
|
|
14
|
+
<main className={styles.main}>
|
|
15
|
+
<Routes>
|
|
16
|
+
<Route path="/" element={<FeedScreen />} />
|
|
17
|
+
<Route path="/article/:id" element={<ArticleScreen />} />
|
|
18
|
+
</Routes>
|
|
19
|
+
</main>
|
|
20
|
+
</div>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useNavigate } from 'react-router-dom';
|
|
3
|
+
import styles from './ArticleCard.module.css';
|
|
4
|
+
|
|
5
|
+
export default function ArticleCard({ article }) {
|
|
6
|
+
const navigate = useNavigate();
|
|
7
|
+
const { id, title, summary, author, publishedAt, readingTime, tag, imageAlt } = article;
|
|
8
|
+
|
|
9
|
+
const formattedDate = new Date(publishedAt).toLocaleDateString('en-US', {
|
|
10
|
+
month: 'short',
|
|
11
|
+
day: 'numeric',
|
|
12
|
+
year: 'numeric',
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
return (
|
|
16
|
+
<article
|
|
17
|
+
className={styles.card}
|
|
18
|
+
onClick={() => navigate(`/article/${id}`)}
|
|
19
|
+
role="button"
|
|
20
|
+
tabIndex={0}
|
|
21
|
+
onKeyDown={e => e.key === 'Enter' && navigate(`/article/${id}`)}
|
|
22
|
+
>
|
|
23
|
+
<div className={styles.imagePlaceholder} aria-label={imageAlt}>
|
|
24
|
+
<span className={styles.imagePlaceholderText}>{imageAlt}</span>
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
<div className={styles.body}>
|
|
28
|
+
<div className={styles.meta}>
|
|
29
|
+
<span className={styles.tag}>{tag}</span>
|
|
30
|
+
<span className={styles.readingTime}>{readingTime} min read</span>
|
|
31
|
+
</div>
|
|
32
|
+
|
|
33
|
+
<h2 className={styles.title}>{title}</h2>
|
|
34
|
+
<p className={styles.summary}>{summary}</p>
|
|
35
|
+
|
|
36
|
+
<div className={styles.footer}>
|
|
37
|
+
<div className={styles.authorArea}>
|
|
38
|
+
<div className={styles.avatar} aria-hidden="true">
|
|
39
|
+
{author.name.slice(0, 1)}
|
|
40
|
+
</div>
|
|
41
|
+
<div className={styles.authorInfo}>
|
|
42
|
+
<span className={styles.authorName}>{author.name}</span>
|
|
43
|
+
<span className={styles.authorRole}>{author.role}</span>
|
|
44
|
+
</div>
|
|
45
|
+
</div>
|
|
46
|
+
<time className={styles.date} dateTime={publishedAt}>
|
|
47
|
+
{formattedDate}
|
|
48
|
+
</time>
|
|
49
|
+
</div>
|
|
50
|
+
</div>
|
|
51
|
+
</article>
|
|
52
|
+
);
|
|
53
|
+
}
|