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,294 @@
1
+ .article {
2
+ max-width: 720px;
3
+ margin: 0 auto;
4
+ padding: var(--space-8) var(--space-6) var(--space-12);
5
+ }
6
+
7
+ /* ── Top bar ──────────────────────────────────────────────────── */
8
+ .topBar {
9
+ display: flex;
10
+ flex-direction: row;
11
+ align-items: center;
12
+ justify-content: space-between;
13
+ margin-bottom: var(--space-8);
14
+ }
15
+
16
+ .backButton {
17
+ font-size: var(--font-size-sm);
18
+ font-weight: var(--font-weight-medium);
19
+ color: var(--color-text-accent);
20
+ padding: var(--space-2) var(--space-3);
21
+ border-radius: var(--radius-full);
22
+ border: 1px solid var(--color-border);
23
+ transition: background-color var(--duration-fast) var(--easing-default);
24
+ }
25
+
26
+ .backButton:hover {
27
+ background-color: var(--color-bg-secondary);
28
+ }
29
+
30
+ .tagChip {
31
+ font-size: var(--font-size-xs);
32
+ font-weight: var(--font-weight-semibold);
33
+ color: var(--color-text-accent);
34
+ text-transform: uppercase;
35
+ letter-spacing: 0.6px;
36
+ padding: var(--space-1) var(--space-3);
37
+ background-color: var(--color-bg-tag);
38
+ border-radius: var(--radius-full);
39
+ }
40
+
41
+ /* ── Header ───────────────────────────────────────────────────── */
42
+ .header {
43
+ display: flex;
44
+ flex-direction: column;
45
+ gap: var(--space-5);
46
+ margin-bottom: var(--space-10);
47
+ padding-bottom: var(--space-8);
48
+ border-bottom: 1px solid var(--color-divider);
49
+ }
50
+
51
+ .title {
52
+ font-family: var(--font-family-display);
53
+ font-size: var(--font-size-display);
54
+ font-weight: var(--font-weight-heavy);
55
+ color: var(--color-text-primary);
56
+ line-height: var(--line-height-tight);
57
+ letter-spacing: -1.5px;
58
+ }
59
+
60
+ .subtitle {
61
+ font-size: var(--font-size-subhead);
62
+ font-weight: var(--font-weight-regular);
63
+ color: var(--color-text-secondary);
64
+ line-height: var(--line-height-relaxed);
65
+ }
66
+
67
+ .byline {
68
+ display: flex;
69
+ flex-direction: row;
70
+ align-items: center;
71
+ gap: var(--space-3);
72
+ }
73
+
74
+ .avatarLg {
75
+ width: 44px;
76
+ height: 44px;
77
+ border-radius: var(--radius-full);
78
+ background-color: var(--color-accent);
79
+ color: var(--color-text-on-accent);
80
+ font-size: var(--font-size-body-lg);
81
+ font-weight: var(--font-weight-bold);
82
+ display: flex;
83
+ align-items: center;
84
+ justify-content: center;
85
+ flex-shrink: 0;
86
+ }
87
+
88
+ .bylineText {
89
+ display: flex;
90
+ flex-direction: column;
91
+ gap: 2px;
92
+ }
93
+
94
+ .authorName {
95
+ font-size: var(--font-size-body);
96
+ font-weight: var(--font-weight-semibold);
97
+ color: var(--color-text-primary);
98
+ }
99
+
100
+ .authorMeta {
101
+ font-size: var(--font-size-sm);
102
+ color: var(--color-text-caption);
103
+ }
104
+
105
+ /* ── Body ─────────────────────────────────────────────────────── */
106
+ .body {
107
+ display: flex;
108
+ flex-direction: column;
109
+ gap: var(--space-5);
110
+ }
111
+
112
+ /* Paragraph — main body text */
113
+ .paragraph {
114
+ font-size: var(--font-size-body-lg);
115
+ font-weight: var(--font-weight-regular);
116
+ color: var(--color-text-primary);
117
+ line-height: var(--line-height-relaxed);
118
+ }
119
+
120
+ /* Section heading */
121
+ .heading2 {
122
+ font-family: var(--font-family-display);
123
+ font-size: var(--font-size-title);
124
+ font-weight: var(--font-weight-bold);
125
+ color: var(--color-text-primary);
126
+ line-height: var(--line-height-tight);
127
+ letter-spacing: -0.8px;
128
+ margin-top: var(--space-4);
129
+ }
130
+
131
+ /* Sub-section heading */
132
+ .heading3 {
133
+ font-family: var(--font-family-display);
134
+ font-size: var(--font-size-headline);
135
+ font-weight: var(--font-weight-semibold);
136
+ color: var(--color-text-primary);
137
+ line-height: var(--line-height-tight);
138
+ letter-spacing: -0.4px;
139
+ margin-top: var(--space-2);
140
+ }
141
+
142
+ /* Pull-quote */
143
+ .pullquote {
144
+ border-left: 3px solid var(--color-accent);
145
+ padding: var(--space-4) var(--space-6);
146
+ background-color: var(--color-bg-secondary);
147
+ border-radius: 0 var(--radius-md) var(--radius-md) 0;
148
+ display: flex;
149
+ flex-direction: column;
150
+ gap: var(--space-3);
151
+ }
152
+
153
+ .pullquoteText {
154
+ font-family: var(--font-family-display);
155
+ font-size: var(--font-size-subhead);
156
+ font-weight: var(--font-weight-medium);
157
+ color: var(--color-text-primary);
158
+ line-height: var(--line-height-relaxed);
159
+ font-style: italic;
160
+ }
161
+
162
+ .pullquoteAttribution {
163
+ font-size: var(--font-size-sm);
164
+ font-weight: var(--font-weight-semibold);
165
+ color: var(--color-text-secondary);
166
+ font-style: normal;
167
+ }
168
+
169
+ /* Code block */
170
+ .codeFigure {
171
+ display: flex;
172
+ flex-direction: column;
173
+ gap: var(--space-2);
174
+ }
175
+
176
+ .codeCaption {
177
+ font-size: var(--font-size-xs);
178
+ font-weight: var(--font-weight-semibold);
179
+ color: var(--color-text-secondary);
180
+ text-transform: uppercase;
181
+ letter-spacing: 0.5px;
182
+ }
183
+
184
+ .pre {
185
+ background-color: var(--color-bg-secondary);
186
+ border: 1px solid var(--color-border);
187
+ border-radius: var(--radius-md);
188
+ padding: var(--space-5);
189
+ overflow-x: auto;
190
+ }
191
+
192
+ .code {
193
+ font-family: var(--font-family-mono);
194
+ font-size: var(--font-size-sm);
195
+ color: var(--color-text-primary);
196
+ line-height: var(--line-height-relaxed);
197
+ white-space: pre;
198
+ }
199
+
200
+ /* Caption / footnote */
201
+ .caption {
202
+ font-size: var(--font-size-sm);
203
+ color: var(--color-text-caption);
204
+ line-height: var(--line-height-normal);
205
+ text-align: center;
206
+ }
207
+
208
+ /* Divider */
209
+ .divider {
210
+ border: none;
211
+ border-top: 1px solid var(--color-divider);
212
+ margin: var(--space-2) 0;
213
+ }
214
+
215
+ /* ── Footer / author card ─────────────────────────────────────── */
216
+ .footer {
217
+ margin-top: var(--space-12);
218
+ display: flex;
219
+ flex-direction: column;
220
+ gap: var(--space-6);
221
+ }
222
+
223
+ .footerDivider {
224
+ height: 1px;
225
+ background-color: var(--color-divider);
226
+ }
227
+
228
+ .authorCard {
229
+ display: flex;
230
+ flex-direction: row;
231
+ gap: var(--space-4);
232
+ align-items: flex-start;
233
+ padding: var(--space-5);
234
+ background-color: var(--color-bg-secondary);
235
+ border-radius: var(--radius-lg);
236
+ }
237
+
238
+ .authorCardText {
239
+ display: flex;
240
+ flex-direction: column;
241
+ gap: var(--space-1);
242
+ }
243
+
244
+ .authorCardName {
245
+ font-size: var(--font-size-body);
246
+ font-weight: var(--font-weight-bold);
247
+ color: var(--color-text-primary);
248
+ }
249
+
250
+ .authorCardRole {
251
+ font-size: var(--font-size-sm);
252
+ color: var(--color-text-secondary);
253
+ }
254
+
255
+ .authorBio {
256
+ font-size: var(--font-size-body);
257
+ color: var(--color-text-secondary);
258
+ line-height: var(--line-height-normal);
259
+ margin-top: var(--space-2);
260
+ }
261
+
262
+ /* ── Loading / error states ───────────────────────────────────── */
263
+ .stateContainer {
264
+ display: flex;
265
+ flex-direction: column;
266
+ align-items: center;
267
+ gap: var(--space-3);
268
+ padding: var(--space-12) var(--space-6);
269
+ }
270
+
271
+ .spinner {
272
+ width: 32px;
273
+ height: 32px;
274
+ border: 3px solid var(--color-border);
275
+ border-top-color: var(--color-accent);
276
+ border-radius: var(--radius-full);
277
+ animation: spin 0.8s linear infinite;
278
+ }
279
+
280
+ @keyframes spin {
281
+ to { transform: rotate(360deg); }
282
+ }
283
+
284
+ .stateText {
285
+ font-size: var(--font-size-body);
286
+ color: var(--color-text-secondary);
287
+ text-align: center;
288
+ }
289
+
290
+ .errorHeading {
291
+ font-size: var(--font-size-headline);
292
+ font-weight: var(--font-weight-semibold);
293
+ color: var(--color-danger);
294
+ }
@@ -0,0 +1,147 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import ArticleCard from '../components/ArticleCard.jsx';
3
+ import TagCloud from '../components/TagCloud.jsx';
4
+ import TrendChart from '../components/TrendChart.jsx';
5
+ import styles from './FeedScreen.module.css';
6
+
7
+ const ALL_TAGS = [
8
+ 'Technology',
9
+ 'Design',
10
+ 'Engineering',
11
+ 'Swift',
12
+ 'SwiftUI',
13
+ 'Machine Learning',
14
+ 'Web Performance',
15
+ 'Accessibility',
16
+ 'Open Source',
17
+ 'Developer Experience',
18
+ 'iOS 18',
19
+ 'WWDC',
20
+ ];
21
+
22
+ const TREND_DATA = [12, 19, 8, 24, 17, 31, 28, 35, 22, 40, 38, 45];
23
+
24
+ export default function FeedScreen() {
25
+ const [articles, setArticles] = useState([]);
26
+ const [loading, setLoading] = useState(true);
27
+ const [error, setError] = useState(null);
28
+ const [selectedTag, setSelectedTag] = useState(null);
29
+
30
+ useEffect(() => {
31
+ let cancelled = false;
32
+
33
+ async function loadFeed() {
34
+ setLoading(true);
35
+ setError(null);
36
+ try {
37
+ // Fetch from mock API endpoint (relative URL; real server returns JSON)
38
+ const res = await fetch('/api/articles');
39
+ if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
40
+ const data = await res.json();
41
+ if (!cancelled) setArticles(data.articles ?? []);
42
+ } catch (err) {
43
+ if (!cancelled) setError(err.message);
44
+ } finally {
45
+ if (!cancelled) setLoading(false);
46
+ }
47
+ }
48
+
49
+ loadFeed();
50
+ return () => { cancelled = true; };
51
+ }, []);
52
+
53
+ const filtered = selectedTag
54
+ ? articles.filter(a => a.tag === selectedTag)
55
+ : articles;
56
+
57
+ return (
58
+ <div className={styles.screen}>
59
+ {/* ── Hero header ───────────────────────────────────────── */}
60
+ <section className={styles.hero}>
61
+ <div className={styles.heroInner}>
62
+ <p className={styles.heroEyebrow}>Daily Edition — May 2026</p>
63
+ <h1 className={styles.heroTitle}>The Chronicle</h1>
64
+ <p className={styles.heroSubtitle}>
65
+ Thoughtful writing on technology, design, and the craft of building software.
66
+ </p>
67
+ </div>
68
+ </section>
69
+
70
+ <div className={styles.content}>
71
+ {/* ── Trend chart — TIER3 surface ───────────────────── */}
72
+ {/* TIER3: WebGL canvas chart via TrendChart component.
73
+ This component uses WebGLRenderingContext (GLSL shaders) for
74
+ GPU-accelerated sparkline rendering. It cannot be transpiled to
75
+ SwiftUI; must be replaced with Swift Charts LineChart or WKWebView.
76
+ See TrendChart.jsx for full TIER3 annotation. */}
77
+ <section className={styles.trendSection}>
78
+ <TrendChart
79
+ data={TREND_DATA}
80
+ label="Reader engagement — last 12 weeks"
81
+ color="var(--color-accent)"
82
+ />
83
+ </section>
84
+
85
+ {/* ── Tag filter ────────────────────────────────────── */}
86
+ <section className={styles.tagsSection}>
87
+ <h2 className={styles.sectionHeading}>Topics</h2>
88
+ {/* CUSTOM-LAYOUT: flex-wrap:wrap (see TagCloud.module.css) */}
89
+ <TagCloud
90
+ tags={ALL_TAGS}
91
+ selectedTag={selectedTag}
92
+ onTagClick={setSelectedTag}
93
+ />
94
+ </section>
95
+
96
+ {/* ── Articles grid ─────────────────────────────────── */}
97
+ <section className={styles.feedSection}>
98
+ <div className={styles.feedHeader}>
99
+ <h2 className={styles.sectionHeading}>
100
+ {selectedTag ? `#${selectedTag}` : 'Latest'}
101
+ </h2>
102
+ {selectedTag && (
103
+ <button
104
+ className={styles.clearFilter}
105
+ onClick={() => setSelectedTag(null)}
106
+ >
107
+ Clear filter
108
+ </button>
109
+ )}
110
+ </div>
111
+
112
+ {loading && (
113
+ <div className={styles.stateContainer}>
114
+ <div className={styles.spinner} aria-label="Loading articles" />
115
+ <p className={styles.stateText}>Loading articles…</p>
116
+ </div>
117
+ )}
118
+
119
+ {error && !loading && (
120
+ <div className={styles.stateContainer} role="alert">
121
+ <p className={styles.errorHeading}>Failed to load feed</p>
122
+ <p className={styles.stateText}>{error}</p>
123
+ </div>
124
+ )}
125
+
126
+ {!loading && !error && filtered.length === 0 && (
127
+ <div className={styles.stateContainer}>
128
+ <p className={styles.stateText}>
129
+ {selectedTag
130
+ ? `No articles tagged "${selectedTag}" yet.`
131
+ : 'No articles found.'}
132
+ </p>
133
+ </div>
134
+ )}
135
+
136
+ {!loading && !error && filtered.length > 0 && (
137
+ <div className={styles.grid}>
138
+ {filtered.map(article => (
139
+ <ArticleCard key={article.id} article={article} />
140
+ ))}
141
+ </div>
142
+ )}
143
+ </section>
144
+ </div>
145
+ </div>
146
+ );
147
+ }
@@ -0,0 +1,161 @@
1
+ .screen {
2
+ display: flex;
3
+ flex-direction: column;
4
+ }
5
+
6
+ /* ── Hero ─────────────────────────────────────────────────────── */
7
+ .hero {
8
+ background: linear-gradient(
9
+ 160deg,
10
+ var(--color-bg-primary) 0%,
11
+ var(--color-bg-secondary) 100%
12
+ );
13
+ border-bottom: 1px solid var(--color-border);
14
+ padding: var(--space-12) var(--space-6) var(--space-10);
15
+ }
16
+
17
+ .heroInner {
18
+ max-width: 680px;
19
+ margin: 0 auto;
20
+ display: flex;
21
+ flex-direction: column;
22
+ gap: var(--space-3);
23
+ }
24
+
25
+ .heroEyebrow {
26
+ font-size: var(--font-size-xs);
27
+ font-weight: var(--font-weight-semibold);
28
+ color: var(--color-text-accent);
29
+ text-transform: uppercase;
30
+ letter-spacing: 1px;
31
+ }
32
+
33
+ .heroTitle {
34
+ font-family: var(--font-family-display);
35
+ font-size: var(--font-size-display);
36
+ font-weight: var(--font-weight-heavy);
37
+ color: var(--color-text-primary);
38
+ line-height: var(--line-height-tight);
39
+ letter-spacing: -1.5px;
40
+ }
41
+
42
+ .heroSubtitle {
43
+ font-size: var(--font-size-body-lg);
44
+ color: var(--color-text-secondary);
45
+ line-height: var(--line-height-relaxed);
46
+ max-width: 520px;
47
+ }
48
+
49
+ /* ── Content container ────────────────────────────────────────── */
50
+ .content {
51
+ max-width: 1100px;
52
+ margin: 0 auto;
53
+ width: 100%;
54
+ padding: var(--space-8) var(--space-6) var(--space-12);
55
+ display: flex;
56
+ flex-direction: column;
57
+ gap: var(--space-8);
58
+ }
59
+
60
+ /* ── Trend chart section ──────────────────────────────────────── */
61
+ .trendSection {
62
+ padding: var(--space-5);
63
+ background-color: var(--color-bg-card);
64
+ border-radius: var(--radius-lg);
65
+ box-shadow: var(--shadow-card);
66
+ border: 1px solid var(--color-border);
67
+ }
68
+
69
+ /* ── Tags section ─────────────────────────────────────────────── */
70
+ .tagsSection {
71
+ display: flex;
72
+ flex-direction: column;
73
+ }
74
+
75
+ .sectionHeading {
76
+ font-family: var(--font-family-display);
77
+ font-size: var(--font-size-headline);
78
+ font-weight: var(--font-weight-bold);
79
+ color: var(--color-text-primary);
80
+ letter-spacing: -0.4px;
81
+ }
82
+
83
+ /* ── Feed section ─────────────────────────────────────────────── */
84
+ .feedSection {
85
+ display: flex;
86
+ flex-direction: column;
87
+ gap: var(--space-5);
88
+ }
89
+
90
+ .feedHeader {
91
+ display: flex;
92
+ flex-direction: row;
93
+ align-items: center;
94
+ justify-content: space-between;
95
+ }
96
+
97
+ .clearFilter {
98
+ font-size: var(--font-size-sm);
99
+ font-weight: var(--font-weight-medium);
100
+ color: var(--color-text-accent);
101
+ padding: var(--space-1) var(--space-3);
102
+ border-radius: var(--radius-full);
103
+ border: 1px solid var(--color-border);
104
+ transition: background-color var(--duration-fast) var(--easing-default);
105
+ }
106
+
107
+ .clearFilter:hover {
108
+ background-color: var(--color-bg-secondary);
109
+ }
110
+
111
+ /* ── Article grid ─────────────────────────────────────────────── */
112
+ /*
113
+ * Responsive masonry-ish grid with auto-fill columns.
114
+ * grid-template-columns: repeat(auto-fill, minmax(320px, 1fr)) creates a
115
+ * flexible grid where column count adapts to viewport width and column widths
116
+ * are non-uniform when items don't fill the row exactly.
117
+ *
118
+ * This maps to LazyVGrid with GridItem(.adaptive(minimum: 320)) in SwiftUI —
119
+ * which is the closest approximation. However if the design requires
120
+ * exact column-span or row-span overrides, a custom Layout is needed.
121
+ */
122
+ .grid {
123
+ display: grid;
124
+ grid-template-columns: repeat(auto-fill, minmax(320px, 1fr));
125
+ gap: var(--space-5);
126
+ align-items: start;
127
+ }
128
+
129
+ /* ── Loading / error / empty states ──────────────────────────── */
130
+ .stateContainer {
131
+ display: flex;
132
+ flex-direction: column;
133
+ align-items: center;
134
+ gap: var(--space-3);
135
+ padding: var(--space-12) var(--space-6);
136
+ }
137
+
138
+ .spinner {
139
+ width: 32px;
140
+ height: 32px;
141
+ border: 3px solid var(--color-border);
142
+ border-top-color: var(--color-accent);
143
+ border-radius: var(--radius-full);
144
+ animation: spin 0.8s linear infinite;
145
+ }
146
+
147
+ @keyframes spin {
148
+ to { transform: rotate(360deg); }
149
+ }
150
+
151
+ .stateText {
152
+ font-size: var(--font-size-body);
153
+ color: var(--color-text-secondary);
154
+ text-align: center;
155
+ }
156
+
157
+ .errorHeading {
158
+ font-size: var(--font-size-headline);
159
+ font-weight: var(--font-weight-semibold);
160
+ color: var(--color-danger);
161
+ }
@@ -0,0 +1,50 @@
1
+ *,
2
+ *::before,
3
+ *::after {
4
+ box-sizing: border-box;
5
+ margin: 0;
6
+ padding: 0;
7
+ }
8
+
9
+ html {
10
+ font-size: 16px;
11
+ -webkit-font-smoothing: antialiased;
12
+ -moz-osx-font-smoothing: grayscale;
13
+ }
14
+
15
+ body {
16
+ font-family: var(--font-family-base);
17
+ font-size: var(--font-size-body);
18
+ color: var(--color-text-primary);
19
+ background-color: var(--color-bg-primary);
20
+ line-height: var(--line-height-normal);
21
+ min-height: 100dvh;
22
+ }
23
+
24
+ #root {
25
+ display: flex;
26
+ flex-direction: column;
27
+ min-height: 100dvh;
28
+ }
29
+
30
+ a {
31
+ color: var(--color-text-accent);
32
+ text-decoration: none;
33
+ }
34
+
35
+ a:hover {
36
+ text-decoration: underline;
37
+ }
38
+
39
+ img {
40
+ max-width: 100%;
41
+ height: auto;
42
+ display: block;
43
+ }
44
+
45
+ button {
46
+ font-family: inherit;
47
+ cursor: pointer;
48
+ border: none;
49
+ background: none;
50
+ }