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
package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/ArticleCard.module.css
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
.card {
|
|
2
|
+
background-color: var(--color-bg-card);
|
|
3
|
+
border-radius: var(--radius-lg);
|
|
4
|
+
box-shadow: var(--shadow-card);
|
|
5
|
+
overflow: hidden;
|
|
6
|
+
cursor: pointer;
|
|
7
|
+
transition: box-shadow var(--duration-normal) var(--easing-default),
|
|
8
|
+
transform var(--duration-normal) var(--easing-default);
|
|
9
|
+
display: flex;
|
|
10
|
+
flex-direction: column;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
.card:hover {
|
|
14
|
+
box-shadow: var(--shadow-elevated);
|
|
15
|
+
transform: translateY(-2px);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
.card:focus-visible {
|
|
19
|
+
outline: 2px solid var(--color-accent);
|
|
20
|
+
outline-offset: 2px;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
.imagePlaceholder {
|
|
24
|
+
width: 100%;
|
|
25
|
+
aspect-ratio: 16 / 9;
|
|
26
|
+
background-color: var(--color-bg-secondary);
|
|
27
|
+
display: flex;
|
|
28
|
+
align-items: center;
|
|
29
|
+
justify-content: center;
|
|
30
|
+
overflow: hidden;
|
|
31
|
+
border-bottom: 1px solid var(--color-divider);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
.imagePlaceholderText {
|
|
35
|
+
font-size: var(--font-size-xs);
|
|
36
|
+
color: var(--color-text-caption);
|
|
37
|
+
text-align: center;
|
|
38
|
+
padding: var(--space-2);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.body {
|
|
42
|
+
padding: var(--space-5);
|
|
43
|
+
display: flex;
|
|
44
|
+
flex-direction: column;
|
|
45
|
+
gap: var(--space-3);
|
|
46
|
+
flex-grow: 1;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.meta {
|
|
50
|
+
display: flex;
|
|
51
|
+
flex-direction: row;
|
|
52
|
+
align-items: center;
|
|
53
|
+
justify-content: space-between;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
.tag {
|
|
57
|
+
font-size: var(--font-size-xs);
|
|
58
|
+
font-weight: var(--font-weight-semibold);
|
|
59
|
+
color: var(--color-text-accent);
|
|
60
|
+
text-transform: uppercase;
|
|
61
|
+
letter-spacing: 0.6px;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.readingTime {
|
|
65
|
+
font-size: var(--font-size-xs);
|
|
66
|
+
color: var(--color-text-caption);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.title {
|
|
70
|
+
font-family: var(--font-family-display);
|
|
71
|
+
font-size: var(--font-size-subhead);
|
|
72
|
+
font-weight: var(--font-weight-bold);
|
|
73
|
+
color: var(--color-text-primary);
|
|
74
|
+
line-height: var(--line-height-tight);
|
|
75
|
+
letter-spacing: -0.3px;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.summary {
|
|
79
|
+
font-size: var(--font-size-body);
|
|
80
|
+
color: var(--color-text-secondary);
|
|
81
|
+
line-height: var(--line-height-normal);
|
|
82
|
+
display: -webkit-box;
|
|
83
|
+
-webkit-line-clamp: 3;
|
|
84
|
+
-webkit-box-orient: vertical;
|
|
85
|
+
overflow: hidden;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
.footer {
|
|
89
|
+
display: flex;
|
|
90
|
+
flex-direction: row;
|
|
91
|
+
align-items: center;
|
|
92
|
+
justify-content: space-between;
|
|
93
|
+
margin-top: auto;
|
|
94
|
+
padding-top: var(--space-3);
|
|
95
|
+
border-top: 1px solid var(--color-divider);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.authorArea {
|
|
99
|
+
display: flex;
|
|
100
|
+
flex-direction: row;
|
|
101
|
+
align-items: center;
|
|
102
|
+
gap: var(--space-2);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.avatar {
|
|
106
|
+
width: 32px;
|
|
107
|
+
height: 32px;
|
|
108
|
+
border-radius: var(--radius-full);
|
|
109
|
+
background-color: var(--color-accent);
|
|
110
|
+
color: var(--color-text-on-accent);
|
|
111
|
+
font-size: var(--font-size-sm);
|
|
112
|
+
font-weight: var(--font-weight-bold);
|
|
113
|
+
display: flex;
|
|
114
|
+
align-items: center;
|
|
115
|
+
justify-content: center;
|
|
116
|
+
flex-shrink: 0;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
.authorInfo {
|
|
120
|
+
display: flex;
|
|
121
|
+
flex-direction: column;
|
|
122
|
+
gap: 1px;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.authorName {
|
|
126
|
+
font-size: var(--font-size-sm);
|
|
127
|
+
font-weight: var(--font-weight-semibold);
|
|
128
|
+
color: var(--color-text-primary);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.authorRole {
|
|
132
|
+
font-size: var(--font-size-xs);
|
|
133
|
+
color: var(--color-text-caption);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
.date {
|
|
137
|
+
font-size: var(--font-size-xs);
|
|
138
|
+
color: var(--color-text-caption);
|
|
139
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { useNavigate, useLocation } from 'react-router-dom';
|
|
3
|
+
import styles from './NavBar.module.css';
|
|
4
|
+
|
|
5
|
+
const TABS = [
|
|
6
|
+
{ id: 'feed', label: 'Feed', path: '/' },
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
export default function NavBar({ activeTab, onTabChange }) {
|
|
10
|
+
const navigate = useNavigate();
|
|
11
|
+
const location = useLocation();
|
|
12
|
+
|
|
13
|
+
function handleTab(tab) {
|
|
14
|
+
onTabChange(tab.id);
|
|
15
|
+
navigate(tab.path);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return (
|
|
19
|
+
<header className={styles.navbar}>
|
|
20
|
+
<div className={styles.logoArea}>
|
|
21
|
+
<span className={styles.logoMark}>C</span>
|
|
22
|
+
<span className={styles.logoText}>Chronicle</span>
|
|
23
|
+
</div>
|
|
24
|
+
<nav className={styles.tabs}>
|
|
25
|
+
{TABS.map(tab => (
|
|
26
|
+
<button
|
|
27
|
+
key={tab.id}
|
|
28
|
+
className={`${styles.tab} ${location.pathname === tab.path ? styles.tabActive : ''}`}
|
|
29
|
+
onClick={() => handleTab(tab)}
|
|
30
|
+
>
|
|
31
|
+
{tab.label}
|
|
32
|
+
</button>
|
|
33
|
+
))}
|
|
34
|
+
</nav>
|
|
35
|
+
</header>
|
|
36
|
+
);
|
|
37
|
+
}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
.navbar {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: row;
|
|
4
|
+
align-items: center;
|
|
5
|
+
justify-content: space-between;
|
|
6
|
+
padding: var(--space-3) var(--space-6);
|
|
7
|
+
background-color: var(--color-bg-primary);
|
|
8
|
+
border-bottom: 1px solid var(--color-border);
|
|
9
|
+
position: sticky;
|
|
10
|
+
top: 0;
|
|
11
|
+
z-index: 100;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
.logoArea {
|
|
15
|
+
display: flex;
|
|
16
|
+
flex-direction: row;
|
|
17
|
+
align-items: center;
|
|
18
|
+
gap: var(--space-2);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
.logoMark {
|
|
22
|
+
width: 28px;
|
|
23
|
+
height: 28px;
|
|
24
|
+
border-radius: var(--radius-sm);
|
|
25
|
+
background-color: var(--color-accent);
|
|
26
|
+
color: var(--color-text-on-accent);
|
|
27
|
+
font-family: var(--font-family-display);
|
|
28
|
+
font-size: var(--font-size-sm);
|
|
29
|
+
font-weight: var(--font-weight-bold);
|
|
30
|
+
display: flex;
|
|
31
|
+
align-items: center;
|
|
32
|
+
justify-content: center;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
.logoText {
|
|
36
|
+
font-family: var(--font-family-display);
|
|
37
|
+
font-size: var(--font-size-headline);
|
|
38
|
+
font-weight: var(--font-weight-semibold);
|
|
39
|
+
color: var(--color-text-primary);
|
|
40
|
+
letter-spacing: -0.5px;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
.tabs {
|
|
44
|
+
display: flex;
|
|
45
|
+
flex-direction: row;
|
|
46
|
+
gap: var(--space-1);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
.tab {
|
|
50
|
+
padding: var(--space-2) var(--space-4);
|
|
51
|
+
border-radius: var(--radius-full);
|
|
52
|
+
font-size: var(--font-size-sm);
|
|
53
|
+
font-weight: var(--font-weight-medium);
|
|
54
|
+
color: var(--color-text-secondary);
|
|
55
|
+
transition: background-color var(--duration-fast) var(--easing-default),
|
|
56
|
+
color var(--duration-fast) var(--easing-default);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.tab:hover {
|
|
60
|
+
background-color: var(--color-bg-secondary);
|
|
61
|
+
color: var(--color-text-primary);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
.tabActive {
|
|
65
|
+
background-color: var(--color-accent);
|
|
66
|
+
color: var(--color-text-on-accent);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
.tabActive:hover {
|
|
70
|
+
background-color: var(--color-accent-hover);
|
|
71
|
+
color: var(--color-text-on-accent);
|
|
72
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import styles from './TagCloud.module.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TagCloud — a flex-wrap tag gallery with non-uniform item widths.
|
|
6
|
+
*
|
|
7
|
+
* CSS uses `flex-wrap: wrap` with variable-length tag labels.
|
|
8
|
+
* This pattern has NO stock SwiftUI equivalent:
|
|
9
|
+
* "flex-wrap: wrap → HStack never wraps; LazyHGrid/LazyVGrid are scroll
|
|
10
|
+
* containers, not inline wrapping layouts" (css-to-swiftui-map.md)
|
|
11
|
+
*
|
|
12
|
+
* CUSTOM-LAYOUT: flex-wrap:wrap — the variable-width tags must reflow onto
|
|
13
|
+
* multiple rows at arbitrary breakpoints determined by available width, which
|
|
14
|
+
* HStack cannot do. SwiftUI port requires the FlowLayout custom Layout protocol.
|
|
15
|
+
*/
|
|
16
|
+
export default function TagCloud({ tags, onTagClick, selectedTag }) {
|
|
17
|
+
return (
|
|
18
|
+
<div className={styles.cloud}>
|
|
19
|
+
{tags.map(tag => (
|
|
20
|
+
<button
|
|
21
|
+
key={tag}
|
|
22
|
+
className={`${styles.tag} ${selectedTag === tag ? styles.tagSelected : ''}`}
|
|
23
|
+
onClick={() => onTagClick(tag === selectedTag ? null : tag)}
|
|
24
|
+
>
|
|
25
|
+
{tag}
|
|
26
|
+
</button>
|
|
27
|
+
))}
|
|
28
|
+
</div>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/* CUSTOM-LAYOUT: flex-wrap:wrap
|
|
2
|
+
*
|
|
3
|
+
* flex-wrap: wrap causes tags with non-uniform widths to reflow onto multiple
|
|
4
|
+
* rows. The break points are data-driven (label length) and width-driven
|
|
5
|
+
* (viewport). HStack in SwiftUI never wraps — this MUST use a custom Layout
|
|
6
|
+
* (FlowLayout) implementing the Layout protocol (iOS 16+).
|
|
7
|
+
*
|
|
8
|
+
* Trigger quote from css-to-swiftui-map.md:
|
|
9
|
+
* "flex-wrap: wrap → Custom Layout protocol (iOS 16+) — FlowLayout
|
|
10
|
+
* No LazyHGrid/LazyVGrid equivalent for true wrapping"
|
|
11
|
+
*/
|
|
12
|
+
.cloud {
|
|
13
|
+
display: flex;
|
|
14
|
+
flex-direction: row;
|
|
15
|
+
flex-wrap: wrap; /* CUSTOM-LAYOUT: flex-wrap:wrap */
|
|
16
|
+
gap: var(--space-2) var(--space-2);
|
|
17
|
+
padding: var(--space-4) 0;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
.tag {
|
|
21
|
+
display: inline-flex;
|
|
22
|
+
align-items: center;
|
|
23
|
+
padding: var(--space-1) var(--space-3);
|
|
24
|
+
border-radius: var(--radius-full);
|
|
25
|
+
background-color: var(--color-bg-tag);
|
|
26
|
+
color: var(--color-text-accent);
|
|
27
|
+
font-size: var(--font-size-sm);
|
|
28
|
+
font-weight: var(--font-weight-medium);
|
|
29
|
+
white-space: nowrap;
|
|
30
|
+
border: 1px solid transparent;
|
|
31
|
+
transition: background-color var(--duration-fast) var(--easing-default),
|
|
32
|
+
border-color var(--duration-fast) var(--easing-default);
|
|
33
|
+
cursor: pointer;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
.tag:hover {
|
|
37
|
+
background-color: var(--color-bg-secondary);
|
|
38
|
+
border-color: var(--color-border);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
.tagSelected {
|
|
42
|
+
background-color: var(--color-accent);
|
|
43
|
+
color: var(--color-text-on-accent);
|
|
44
|
+
border-color: var(--color-accent);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
.tagSelected:hover {
|
|
48
|
+
background-color: var(--color-accent-hover);
|
|
49
|
+
border-color: var(--color-accent-hover);
|
|
50
|
+
}
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import React, { useRef, useEffect } from 'react';
|
|
2
|
+
import styles from './TrendChart.module.css';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* TrendChart — WebGL sparkline chart rendered on a <canvas> element.
|
|
6
|
+
*
|
|
7
|
+
* {/* TIER3: WebGL canvas — uses WebGLRenderingContext to draw a sparkline.
|
|
8
|
+
* The rendering pipeline is entirely GPU-driven via custom GLSL shaders.
|
|
9
|
+
* There is NO SwiftUI equivalent for arbitrary WebGL content.
|
|
10
|
+
* SwiftUI port options:
|
|
11
|
+
* (a) Replace with a native Swift Charts (iOS 16+) line mark — preferred
|
|
12
|
+
* if data semantics are equivalent.
|
|
13
|
+
* (b) Wrap in WKWebView if exact visual must be preserved (accepts
|
|
14
|
+
* partial fidelity loss: no ProMotion, extra process, no Dynamic Type).
|
|
15
|
+
* Risk triage MUST flag this surface and NOT silently generate wrong code.
|
|
16
|
+
* */
|
|
17
|
+
export default function TrendChart({ data, label, color = '#0071e3' }) {
|
|
18
|
+
const canvasRef = useRef(null);
|
|
19
|
+
|
|
20
|
+
useEffect(() => {
|
|
21
|
+
const canvas = canvasRef.current;
|
|
22
|
+
if (!canvas) return;
|
|
23
|
+
|
|
24
|
+
// Attempt WebGL; fall back to 2D canvas if unavailable (e.g. jsdom in tests)
|
|
25
|
+
const gl = canvas.getContext('webgl') || canvas.getContext('experimental-webgl');
|
|
26
|
+
|
|
27
|
+
if (!gl) {
|
|
28
|
+
// Graceful 2D fallback (no WebGL in test env)
|
|
29
|
+
const ctx = canvas.getContext('2d');
|
|
30
|
+
if (!ctx || !data?.length) return;
|
|
31
|
+
drawFallback2D(ctx, canvas, data, color);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// WebGL sparkline — minimal shader pair
|
|
36
|
+
const vsSource = `
|
|
37
|
+
attribute vec2 a_position;
|
|
38
|
+
uniform vec2 u_resolution;
|
|
39
|
+
void main() {
|
|
40
|
+
vec2 clip = (a_position / u_resolution) * 2.0 - 1.0;
|
|
41
|
+
gl_Position = vec4(clip.x, -clip.y, 0.0, 1.0);
|
|
42
|
+
}
|
|
43
|
+
`;
|
|
44
|
+
|
|
45
|
+
const fsSource = `
|
|
46
|
+
precision mediump float;
|
|
47
|
+
uniform vec4 u_color;
|
|
48
|
+
void main() {
|
|
49
|
+
gl_FragColor = u_color;
|
|
50
|
+
}
|
|
51
|
+
`;
|
|
52
|
+
|
|
53
|
+
const vs = compileShader(gl, gl.VERTEX_SHADER, vsSource);
|
|
54
|
+
const fs = compileShader(gl, gl.FRAGMENT_SHADER, fsSource);
|
|
55
|
+
if (!vs || !fs) return;
|
|
56
|
+
|
|
57
|
+
const program = gl.createProgram();
|
|
58
|
+
gl.attachShader(program, vs);
|
|
59
|
+
gl.attachShader(program, fs);
|
|
60
|
+
gl.linkProgram(program);
|
|
61
|
+
if (!gl.getProgramParameter(program, gl.LINK_STATUS)) return;
|
|
62
|
+
|
|
63
|
+
gl.useProgram(program);
|
|
64
|
+
|
|
65
|
+
// Map data to canvas coordinates
|
|
66
|
+
const w = canvas.width;
|
|
67
|
+
const h = canvas.height;
|
|
68
|
+
const min = Math.min(...data);
|
|
69
|
+
const max = Math.max(...data);
|
|
70
|
+
const range = max - min || 1;
|
|
71
|
+
const pad = 8;
|
|
72
|
+
|
|
73
|
+
const vertices = new Float32Array(data.length * 2);
|
|
74
|
+
data.forEach((v, i) => {
|
|
75
|
+
vertices[i * 2] = pad + (i / (data.length - 1)) * (w - pad * 2);
|
|
76
|
+
vertices[i * 2 + 1] = h - pad - ((v - min) / range) * (h - pad * 2);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
const buf = gl.createBuffer();
|
|
80
|
+
gl.bindBuffer(gl.ARRAY_BUFFER, buf);
|
|
81
|
+
gl.bufferData(gl.ARRAY_BUFFER, vertices, gl.STATIC_DRAW);
|
|
82
|
+
|
|
83
|
+
const posLoc = gl.getAttribLocation(program, 'a_position');
|
|
84
|
+
gl.enableVertexAttribArray(posLoc);
|
|
85
|
+
gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
|
|
86
|
+
|
|
87
|
+
const resLoc = gl.getUniformLocation(program, 'u_resolution');
|
|
88
|
+
gl.uniform2f(resLoc, w, h);
|
|
89
|
+
|
|
90
|
+
const colLoc = gl.getUniformLocation(program, 'u_color');
|
|
91
|
+
const [r, g, b] = hexToRgb(color);
|
|
92
|
+
gl.uniform4f(colLoc, r, g, b, 1.0);
|
|
93
|
+
|
|
94
|
+
gl.viewport(0, 0, w, h);
|
|
95
|
+
gl.clearColor(0, 0, 0, 0);
|
|
96
|
+
gl.clear(gl.COLOR_BUFFER_BIT);
|
|
97
|
+
gl.lineWidth(2);
|
|
98
|
+
gl.drawArrays(gl.LINE_STRIP, 0, data.length);
|
|
99
|
+
|
|
100
|
+
return () => {
|
|
101
|
+
gl.deleteProgram(program);
|
|
102
|
+
gl.deleteBuffer(buf);
|
|
103
|
+
};
|
|
104
|
+
}, [data, color]);
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<div className={styles.chartWrapper}>
|
|
108
|
+
{label && <span className={styles.chartLabel}>{label}</span>}
|
|
109
|
+
<canvas
|
|
110
|
+
ref={canvasRef}
|
|
111
|
+
width={280}
|
|
112
|
+
height={80}
|
|
113
|
+
className={styles.canvas}
|
|
114
|
+
aria-label={`Trend chart: ${label}`}
|
|
115
|
+
role="img"
|
|
116
|
+
/>
|
|
117
|
+
</div>
|
|
118
|
+
);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// ── helpers ──────────────────────────────────────────────────────────────────
|
|
122
|
+
|
|
123
|
+
function compileShader(gl, type, source) {
|
|
124
|
+
const shader = gl.createShader(type);
|
|
125
|
+
gl.shaderSource(shader, source);
|
|
126
|
+
gl.compileShader(shader);
|
|
127
|
+
if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
|
|
128
|
+
gl.deleteShader(shader);
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
return shader;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
function hexToRgb(hex) {
|
|
135
|
+
const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
|
|
136
|
+
return result
|
|
137
|
+
? [parseInt(result[1], 16) / 255, parseInt(result[2], 16) / 255, parseInt(result[3], 16) / 255]
|
|
138
|
+
: [0, 0.44, 0.89];
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function drawFallback2D(ctx, canvas, data, color) {
|
|
142
|
+
const w = canvas.width;
|
|
143
|
+
const h = canvas.height;
|
|
144
|
+
const min = Math.min(...data);
|
|
145
|
+
const max = Math.max(...data);
|
|
146
|
+
const range = max - min || 1;
|
|
147
|
+
const pad = 8;
|
|
148
|
+
|
|
149
|
+
ctx.clearRect(0, 0, w, h);
|
|
150
|
+
ctx.beginPath();
|
|
151
|
+
ctx.strokeStyle = color;
|
|
152
|
+
ctx.lineWidth = 2;
|
|
153
|
+
data.forEach((v, i) => {
|
|
154
|
+
const x = pad + (i / (data.length - 1)) * (w - pad * 2);
|
|
155
|
+
const y = h - pad - ((v - min) / range) * (h - pad * 2);
|
|
156
|
+
i === 0 ? ctx.moveTo(x, y) : ctx.lineTo(x, y);
|
|
157
|
+
});
|
|
158
|
+
ctx.stroke();
|
|
159
|
+
}
|
package/.local/skills/h5-to-swiftui/assets/sample-h5-react/src/components/TrendChart.module.css
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
.chartWrapper {
|
|
2
|
+
display: flex;
|
|
3
|
+
flex-direction: column;
|
|
4
|
+
gap: var(--space-2);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
.chartLabel {
|
|
8
|
+
font-size: var(--font-size-xs);
|
|
9
|
+
font-weight: var(--font-weight-semibold);
|
|
10
|
+
color: var(--color-text-secondary);
|
|
11
|
+
text-transform: uppercase;
|
|
12
|
+
letter-spacing: 0.5px;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
.canvas {
|
|
16
|
+
display: block;
|
|
17
|
+
border-radius: var(--radius-sm);
|
|
18
|
+
background-color: var(--color-bg-secondary);
|
|
19
|
+
width: 100%;
|
|
20
|
+
height: 80px;
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { BrowserRouter } from 'react-router-dom';
|
|
4
|
+
import App from './App.jsx';
|
|
5
|
+
|
|
6
|
+
ReactDOM.createRoot(document.getElementById('root')).render(
|
|
7
|
+
<React.StrictMode>
|
|
8
|
+
<BrowserRouter>
|
|
9
|
+
<App />
|
|
10
|
+
</BrowserRouter>
|
|
11
|
+
</React.StrictMode>
|
|
12
|
+
);
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { useParams, useNavigate } from 'react-router-dom';
|
|
3
|
+
import styles from './ArticleScreen.module.css';
|
|
4
|
+
|
|
5
|
+
export default function ArticleScreen() {
|
|
6
|
+
const { id } = useParams();
|
|
7
|
+
const navigate = useNavigate();
|
|
8
|
+
const [article, setArticle] = useState(null);
|
|
9
|
+
const [loading, setLoading] = useState(true);
|
|
10
|
+
const [error, setError] = useState(null);
|
|
11
|
+
|
|
12
|
+
useEffect(() => {
|
|
13
|
+
let cancelled = false;
|
|
14
|
+
|
|
15
|
+
async function loadArticle() {
|
|
16
|
+
setLoading(true);
|
|
17
|
+
setError(null);
|
|
18
|
+
try {
|
|
19
|
+
const res = await fetch(`/api/articles/${id}`);
|
|
20
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}: ${res.statusText}`);
|
|
21
|
+
const data = await res.json();
|
|
22
|
+
if (!cancelled) setArticle(data);
|
|
23
|
+
} catch (err) {
|
|
24
|
+
if (!cancelled) setError(err.message);
|
|
25
|
+
} finally {
|
|
26
|
+
if (!cancelled) setLoading(false);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
loadArticle();
|
|
31
|
+
return () => { cancelled = true; };
|
|
32
|
+
}, [id]);
|
|
33
|
+
|
|
34
|
+
if (loading) {
|
|
35
|
+
return (
|
|
36
|
+
<div className={styles.stateContainer}>
|
|
37
|
+
<div className={styles.spinner} aria-label="Loading article" />
|
|
38
|
+
<p className={styles.stateText}>Loading article…</p>
|
|
39
|
+
</div>
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (error) {
|
|
44
|
+
return (
|
|
45
|
+
<div className={styles.stateContainer} role="alert">
|
|
46
|
+
<p className={styles.errorHeading}>Could not load article</p>
|
|
47
|
+
<p className={styles.stateText}>{error}</p>
|
|
48
|
+
<button className={styles.backButton} onClick={() => navigate('/')}>
|
|
49
|
+
← Back to feed
|
|
50
|
+
</button>
|
|
51
|
+
</div>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (!article) return null;
|
|
56
|
+
|
|
57
|
+
const {
|
|
58
|
+
title,
|
|
59
|
+
subtitle,
|
|
60
|
+
author,
|
|
61
|
+
publishedAt,
|
|
62
|
+
readingTime,
|
|
63
|
+
tag,
|
|
64
|
+
body,
|
|
65
|
+
} = article;
|
|
66
|
+
|
|
67
|
+
const formattedDate = new Date(publishedAt).toLocaleDateString('en-US', {
|
|
68
|
+
month: 'long',
|
|
69
|
+
day: 'numeric',
|
|
70
|
+
year: 'numeric',
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
return (
|
|
74
|
+
<article className={styles.article}>
|
|
75
|
+
{/* ── Back navigation ───────────────────────────────────── */}
|
|
76
|
+
<div className={styles.topBar}>
|
|
77
|
+
<button className={styles.backButton} onClick={() => navigate('/')}>
|
|
78
|
+
← Chronicle
|
|
79
|
+
</button>
|
|
80
|
+
<span className={styles.tagChip}>{tag}</span>
|
|
81
|
+
</div>
|
|
82
|
+
|
|
83
|
+
{/* ── Article header ────────────────────────────────────── */}
|
|
84
|
+
<header className={styles.header}>
|
|
85
|
+
<h1 className={styles.title}>{title}</h1>
|
|
86
|
+
{subtitle && (
|
|
87
|
+
<p className={styles.subtitle}>{subtitle}</p>
|
|
88
|
+
)}
|
|
89
|
+
|
|
90
|
+
<div className={styles.byline}>
|
|
91
|
+
<div className={styles.avatarLg} aria-hidden="true">
|
|
92
|
+
{author.name.slice(0, 1)}
|
|
93
|
+
</div>
|
|
94
|
+
<div className={styles.bylineText}>
|
|
95
|
+
<span className={styles.authorName}>{author.name}</span>
|
|
96
|
+
<span className={styles.authorMeta}>
|
|
97
|
+
{author.role} · <time dateTime={publishedAt}>{formattedDate}</time> · {readingTime} min read
|
|
98
|
+
</span>
|
|
99
|
+
</div>
|
|
100
|
+
</div>
|
|
101
|
+
</header>
|
|
102
|
+
|
|
103
|
+
{/* ── Body — text-heavy, multi-typographic-level region ─── */}
|
|
104
|
+
{/*
|
|
105
|
+
* TEXT REGION NOTE (skill stage 2):
|
|
106
|
+
* This body region is intentionally text-heavy with multiple typographic
|
|
107
|
+
* levels (h2 headlines, pull-quote, body paragraphs, code block, caption).
|
|
108
|
+
* The skill must apply layout-box IoU + token-color ΔE for region
|
|
109
|
+
* comparison, NOT glyph SSIM, because CoreText metrics diverge from
|
|
110
|
+
* WebKit at identical px declarations (see stack-detection.md reason 4).
|
|
111
|
+
*/}
|
|
112
|
+
<div className={styles.body}>
|
|
113
|
+
{body.map((block, idx) => (
|
|
114
|
+
<ArticleBlock key={idx} block={block} />
|
|
115
|
+
))}
|
|
116
|
+
</div>
|
|
117
|
+
|
|
118
|
+
{/* ── Article footer ────────────────────────────────────── */}
|
|
119
|
+
<footer className={styles.footer}>
|
|
120
|
+
<div className={styles.footerDivider} />
|
|
121
|
+
<div className={styles.authorCard}>
|
|
122
|
+
<div className={styles.avatarLg} aria-hidden="true">
|
|
123
|
+
{author.name.slice(0, 1)}
|
|
124
|
+
</div>
|
|
125
|
+
<div className={styles.authorCardText}>
|
|
126
|
+
<span className={styles.authorCardName}>{author.name}</span>
|
|
127
|
+
<span className={styles.authorCardRole}>{author.role}</span>
|
|
128
|
+
{author.bio && (
|
|
129
|
+
<p className={styles.authorBio}>{author.bio}</p>
|
|
130
|
+
)}
|
|
131
|
+
</div>
|
|
132
|
+
</div>
|
|
133
|
+
</footer>
|
|
134
|
+
</article>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// ── Block renderer ────────────────────────────────────────────────────────────
|
|
139
|
+
|
|
140
|
+
function ArticleBlock({ block }) {
|
|
141
|
+
switch (block.type) {
|
|
142
|
+
case 'paragraph':
|
|
143
|
+
return <p className={styles.paragraph}>{block.text}</p>;
|
|
144
|
+
|
|
145
|
+
case 'heading':
|
|
146
|
+
return <h2 className={styles.heading2}>{block.text}</h2>;
|
|
147
|
+
|
|
148
|
+
case 'subheading':
|
|
149
|
+
return <h3 className={styles.heading3}>{block.text}</h3>;
|
|
150
|
+
|
|
151
|
+
case 'pullquote':
|
|
152
|
+
return (
|
|
153
|
+
<blockquote className={styles.pullquote}>
|
|
154
|
+
<p className={styles.pullquoteText}>{block.text}</p>
|
|
155
|
+
{block.attribution && (
|
|
156
|
+
<cite className={styles.pullquoteAttribution}>{block.attribution}</cite>
|
|
157
|
+
)}
|
|
158
|
+
</blockquote>
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
case 'code':
|
|
162
|
+
return (
|
|
163
|
+
<figure className={styles.codeFigure}>
|
|
164
|
+
{block.caption && (
|
|
165
|
+
<figcaption className={styles.codeCaption}>{block.caption}</figcaption>
|
|
166
|
+
)}
|
|
167
|
+
<pre className={styles.pre}>
|
|
168
|
+
<code className={styles.code}>{block.text}</code>
|
|
169
|
+
</pre>
|
|
170
|
+
</figure>
|
|
171
|
+
);
|
|
172
|
+
|
|
173
|
+
case 'caption':
|
|
174
|
+
return <p className={styles.caption}>{block.text}</p>;
|
|
175
|
+
|
|
176
|
+
case 'divider':
|
|
177
|
+
return <hr className={styles.divider} />;
|
|
178
|
+
|
|
179
|
+
default:
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
}
|