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.
Files changed (58) hide show
  1. package/.local/skills/h5-to-swiftui/SKILL.md +201 -0
  2. package/.local/skills/h5-to-swiftui/assets/calibration/README.md +176 -0
  3. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/index.html +52 -0
  4. package/.local/skills/h5-to-swiftui/assets/calibration/h5-twin/style.css +133 -0
  5. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Package.swift +26 -0
  6. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin/Sources/CalibrationScreen/CalibrationScreen.swift +142 -0
  7. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Package.swift +32 -0
  8. package/.local/skills/h5-to-swiftui/assets/calibration/swiftui-twin-divergent/Sources/CalibrationScreenDivergent/CalibrationScreenDivergent.swift +122 -0
  9. package/.local/skills/h5-to-swiftui/assets/calibration/tokens.json +42 -0
  10. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/index.html +14 -0
  11. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/package.json +20 -0
  12. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/001.json +96 -0
  13. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/public/api/articles/index.json +89 -0
  14. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.jsx +22 -0
  15. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/App.module.css +11 -0
  16. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.jsx +53 -0
  17. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css +139 -0
  18. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.jsx +37 -0
  19. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/NavBar.module.css +72 -0
  20. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.jsx +30 -0
  21. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TagCloud.module.css +50 -0
  22. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.jsx +159 -0
  23. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css +21 -0
  24. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/main.jsx +12 -0
  25. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.jsx +182 -0
  26. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/ArticleScreen.module.css +294 -0
  27. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.jsx +147 -0
  28. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/screens/FeedScreen.module.css +161 -0
  29. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/global.css +50 -0
  30. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/styles/tokens.css +103 -0
  31. package/.local/skills/h5-to-swiftui/assets/sample-h5-react/vite.config.js +6 -0
  32. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/data/tasks.js +67 -0
  33. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/index.html +26 -0
  34. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/router.js +73 -0
  35. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/detail.js +164 -0
  36. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/home.js +53 -0
  37. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/screens/list.js +87 -0
  38. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/app.css +342 -0
  39. package/.local/skills/h5-to-swiftui/assets/sample-h5-vanilla/styles/tokens.css +68 -0
  40. package/.local/skills/h5-to-swiftui/references/css-to-swiftui-map.md +205 -0
  41. package/.local/skills/h5-to-swiftui/references/design-token-extraction.md +209 -0
  42. package/.local/skills/h5-to-swiftui/references/high-risk-triage.md +209 -0
  43. package/.local/skills/h5-to-swiftui/references/render-equivalence-calibration.md +193 -0
  44. package/.local/skills/h5-to-swiftui/references/stack-detection.md +160 -0
  45. package/.local/skills/h5-to-swiftui/references/visual-diff-loop-protocol.md +365 -0
  46. package/.local/skills/h5-to-swiftui/scripts/_calib-consts.mjs +150 -0
  47. package/.local/skills/h5-to-swiftui/scripts/_imglib.mjs +547 -0
  48. package/.local/skills/h5-to-swiftui/scripts/_provenance.mjs +123 -0
  49. package/.local/skills/h5-to-swiftui/scripts/calibrate-render.mjs +625 -0
  50. package/.local/skills/h5-to-swiftui/scripts/capture-reference.mjs +386 -0
  51. package/.local/skills/h5-to-swiftui/scripts/detect-stack.mjs +305 -0
  52. package/.local/skills/h5-to-swiftui/scripts/evaluate-convergence.mjs +1093 -0
  53. package/.local/skills/h5-to-swiftui/scripts/extract-tokens.mjs +600 -0
  54. package/.local/skills/h5-to-swiftui/scripts/mark-overlay.mjs +379 -0
  55. package/.local/skills/h5-to-swiftui/scripts/pixel-diff.mjs +530 -0
  56. package/.local/skills/h5-to-swiftui/scripts/sim-screenshot.sh +544 -0
  57. package/bundled/manifest.json +1 -1
  58. 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,11 @@
1
+ .appRoot {
2
+ display: flex;
3
+ flex-direction: column;
4
+ min-height: 100dvh;
5
+ }
6
+
7
+ .main {
8
+ flex: 1;
9
+ overflow-y: auto;
10
+ background-color: var(--color-bg-secondary);
11
+ }
@@ -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
+ }