doe-sdk 0.1.0

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 (127) hide show
  1. package/README.md +509 -0
  2. package/dist/cli/index.d.ts +17 -0
  3. package/dist/cli/index.d.ts.map +1 -0
  4. package/dist/cli/index.js +118 -0
  5. package/dist/cli/index.js.map +1 -0
  6. package/dist/cli/init.d.ts +25 -0
  7. package/dist/cli/init.d.ts.map +1 -0
  8. package/dist/cli/init.js +100 -0
  9. package/dist/cli/init.js.map +1 -0
  10. package/dist/cli/login.d.ts +52 -0
  11. package/dist/cli/login.d.ts.map +1 -0
  12. package/dist/cli/login.js +571 -0
  13. package/dist/cli/login.js.map +1 -0
  14. package/dist/cli/publish.d.ts +27 -0
  15. package/dist/cli/publish.d.ts.map +1 -0
  16. package/dist/cli/publish.js +531 -0
  17. package/dist/cli/publish.js.map +1 -0
  18. package/dist/cli/scaffold.d.ts +18 -0
  19. package/dist/cli/scaffold.d.ts.map +1 -0
  20. package/dist/cli/scaffold.js +252 -0
  21. package/dist/cli/scaffold.js.map +1 -0
  22. package/dist/cli/ui.d.ts +57 -0
  23. package/dist/cli/ui.d.ts.map +1 -0
  24. package/dist/cli/ui.js +339 -0
  25. package/dist/cli/ui.js.map +1 -0
  26. package/dist/cli/validate.d.ts +28 -0
  27. package/dist/cli/validate.d.ts.map +1 -0
  28. package/dist/cli/validate.js +1270 -0
  29. package/dist/cli/validate.js.map +1 -0
  30. package/dist/compat/legacy-adapter.d.ts +198 -0
  31. package/dist/compat/legacy-adapter.d.ts.map +1 -0
  32. package/dist/compat/legacy-adapter.js +318 -0
  33. package/dist/compat/legacy-adapter.js.map +1 -0
  34. package/dist/index.d.ts +14 -0
  35. package/dist/index.d.ts.map +1 -0
  36. package/dist/index.js +16 -0
  37. package/dist/index.js.map +1 -0
  38. package/dist/runtime/client.d.ts +370 -0
  39. package/dist/runtime/client.d.ts.map +1 -0
  40. package/dist/runtime/client.js +470 -0
  41. package/dist/runtime/client.js.map +1 -0
  42. package/dist/runtime/index.d.ts +8 -0
  43. package/dist/runtime/index.d.ts.map +1 -0
  44. package/dist/runtime/index.js +7 -0
  45. package/dist/runtime/index.js.map +1 -0
  46. package/dist/types/api.d.ts +564 -0
  47. package/dist/types/api.d.ts.map +1 -0
  48. package/dist/types/api.js +11 -0
  49. package/dist/types/api.js.map +1 -0
  50. package/dist/types/index.d.ts +12 -0
  51. package/dist/types/index.d.ts.map +1 -0
  52. package/dist/types/index.js +10 -0
  53. package/dist/types/index.js.map +1 -0
  54. package/dist/types/manifest.d.ts +412 -0
  55. package/dist/types/manifest.d.ts.map +1 -0
  56. package/dist/types/manifest.js +42 -0
  57. package/dist/types/manifest.js.map +1 -0
  58. package/dist/types/marketplace-categories.d.ts +9 -0
  59. package/dist/types/marketplace-categories.d.ts.map +1 -0
  60. package/dist/types/marketplace-categories.js +16 -0
  61. package/dist/types/marketplace-categories.js.map +1 -0
  62. package/dist/types/permissions.d.ts +86 -0
  63. package/dist/types/permissions.d.ts.map +1 -0
  64. package/dist/types/permissions.js +295 -0
  65. package/dist/types/permissions.js.map +1 -0
  66. package/dist/types/theme/index.d.ts +7 -0
  67. package/dist/types/theme/index.d.ts.map +1 -0
  68. package/dist/types/theme/index.js +7 -0
  69. package/dist/types/theme/index.js.map +1 -0
  70. package/dist/types/theme/schema.d.ts +1205 -0
  71. package/dist/types/theme/schema.d.ts.map +1 -0
  72. package/dist/types/theme/schema.js +325 -0
  73. package/dist/types/theme/schema.js.map +1 -0
  74. package/dist/types/theme/types.d.ts +648 -0
  75. package/dist/types/theme/types.d.ts.map +1 -0
  76. package/dist/types/theme/types.js +8 -0
  77. package/dist/types/theme/types.js.map +1 -0
  78. package/package.json +75 -0
  79. package/templates/extension/README.md +254 -0
  80. package/templates/extension/icon.png +0 -0
  81. package/templates/extension/index.html +42 -0
  82. package/templates/extension/manifest.json +57 -0
  83. package/templates/extension/package.json +24 -0
  84. package/templates/extension/src/App.tsx +252 -0
  85. package/templates/extension/src/components/OnboardingComplete.tsx +190 -0
  86. package/templates/extension/src/components/OnboardingProgress.tsx +82 -0
  87. package/templates/extension/src/components/OnboardingWelcome.tsx +166 -0
  88. package/templates/extension/src/components/StepContainer.tsx +217 -0
  89. package/templates/extension/src/components/playground/CanvasTab.tsx +24 -0
  90. package/templates/extension/src/components/playground/ConfigTab.tsx +24 -0
  91. package/templates/extension/src/components/playground/EventsTab.tsx +24 -0
  92. package/templates/extension/src/components/playground/InfoTab.tsx +89 -0
  93. package/templates/extension/src/components/playground/NetworkTab.tsx +24 -0
  94. package/templates/extension/src/components/playground/PlaygroundContainer.tsx +184 -0
  95. package/templates/extension/src/components/playground/ResultDisplay.tsx +30 -0
  96. package/templates/extension/src/components/playground/StorageTab.tsx +24 -0
  97. package/templates/extension/src/components/playground/UITab.tsx +24 -0
  98. package/templates/extension/src/components/shared/CanvasControls.tsx +130 -0
  99. package/templates/extension/src/components/shared/ConfigControls.tsx +154 -0
  100. package/templates/extension/src/components/shared/EventsControls.tsx +232 -0
  101. package/templates/extension/src/components/shared/InfoControls.tsx +281 -0
  102. package/templates/extension/src/components/shared/NetworkControls.tsx +328 -0
  103. package/templates/extension/src/components/shared/StorageControls.tsx +203 -0
  104. package/templates/extension/src/components/shared/UIControls.tsx +199 -0
  105. package/templates/extension/src/components/shared/index.ts +15 -0
  106. package/templates/extension/src/components/steps/CanvasStep.tsx +67 -0
  107. package/templates/extension/src/components/steps/ClipboardStep.tsx +167 -0
  108. package/templates/extension/src/components/steps/ConfigStep.tsx +63 -0
  109. package/templates/extension/src/components/steps/EventsStep.tsx +69 -0
  110. package/templates/extension/src/components/steps/InfoStep.tsx +70 -0
  111. package/templates/extension/src/components/steps/NetworkStep.tsx +70 -0
  112. package/templates/extension/src/components/steps/StorageStep.tsx +61 -0
  113. package/templates/extension/src/components/steps/UIStep.tsx +70 -0
  114. package/templates/extension/src/hooks/useDoe.ts +93 -0
  115. package/templates/extension/src/hooks/useOnboarding.ts +264 -0
  116. package/templates/extension/src/hooks/usePointerHandlers.ts +105 -0
  117. package/templates/extension/src/main.tsx +18 -0
  118. package/templates/extension/src/styles.ts +265 -0
  119. package/templates/extension/tsconfig.json +28 -0
  120. package/templates/extension/vite.config.ts +32 -0
  121. package/templates/theme/README.md +132 -0
  122. package/templates/theme/manifest.json +19 -0
  123. package/templates/theme/package.json +16 -0
  124. package/templates/theme/styles/.gitkeep +2 -0
  125. package/templates/theme/themes/theme.json +32 -0
  126. package/templates/theme/vite-plugin-doe-theme.ts +53 -0
  127. package/templates/theme/vite.config.ts +10 -0
@@ -0,0 +1,281 @@
1
+ /**
2
+ * InfoControls - Shared Workspace + Theme info controls
3
+ *
4
+ * Used by both InfoStep (onboarding) and InfoTab (sandbox)
5
+ */
6
+ import React, { useState, useCallback, useEffect } from "react";
7
+ import type { DOEExtensionAPI } from "doe-sdk/runtime";
8
+ import type { Theme, WorkspaceInfo, UserBasicInfo } from "doe-sdk";
9
+ import { useInteractivePointerDown } from "../../hooks/usePointerHandlers";
10
+ import { spacing, colors, typography, buttonStyles, cardStyles } from "../../styles";
11
+
12
+ export interface InfoControlsProps {
13
+ api: DOEExtensionAPI;
14
+ /** Callback when result changes (for parent to display) */
15
+ onResult?: (result: string) => void;
16
+ }
17
+
18
+ export function InfoControls({
19
+ api,
20
+ onResult,
21
+ }: InfoControlsProps): React.ReactElement {
22
+ const [workspace, setWorkspace] = useState<WorkspaceInfo | null>(null);
23
+ const [user, setUser] = useState<UserBasicInfo | null>(null);
24
+ const [theme, setTheme] = useState<Theme | null>(null);
25
+ const handlePointerDown = useInteractivePointerDown();
26
+
27
+ const setResult = useCallback(
28
+ (msg: string) => {
29
+ onResult?.(msg);
30
+ },
31
+ [onResult]
32
+ );
33
+
34
+ // Load all info on mount
35
+ useEffect(() => {
36
+ const loadInfo = async () => {
37
+ try {
38
+ const [ws, usr, thm] = await Promise.all([
39
+ api.workspace.getInfo(),
40
+ api.workspace.getCurrentUser(),
41
+ api.ui.getTheme(),
42
+ ]);
43
+ setWorkspace(ws);
44
+ setUser(usr);
45
+ setTheme(thm);
46
+ setResult("Loaded workspace, user, and theme info");
47
+ } catch (error) {
48
+ setResult(`Error: ${error instanceof Error ? error.message : "Unknown"}`);
49
+ }
50
+ };
51
+ loadInfo();
52
+
53
+ // Subscribe to theme changes
54
+ const unsubscribe = api.events.onThemeChange((payload) => {
55
+ api.ui.getTheme().then(setTheme);
56
+ setResult(`Theme changed to ${payload.mode}`);
57
+ });
58
+
59
+ return unsubscribe;
60
+ }, [api, setResult]);
61
+
62
+ const refreshAll = useCallback(async () => {
63
+ try {
64
+ const [ws, usr, thm] = await Promise.all([
65
+ api.workspace.getInfo(),
66
+ api.workspace.getCurrentUser(),
67
+ api.ui.getTheme(),
68
+ ]);
69
+ setWorkspace(ws);
70
+ setUser(usr);
71
+ setTheme(thm);
72
+ setResult("Refreshed all info");
73
+ } catch (error) {
74
+ setResult(`Error: ${error instanceof Error ? error.message : "Unknown"}`);
75
+ }
76
+ }, [api, setResult]);
77
+
78
+ return (
79
+ <div>
80
+ {/* Workspace info */}
81
+ <div style={{ marginBottom: spacing.md }}>
82
+ <div
83
+ style={{
84
+ fontSize: typography.sizes.sm,
85
+ fontWeight: typography.weights.medium,
86
+ color: colors.textSecondary,
87
+ marginBottom: spacing.xs,
88
+ }}
89
+ >
90
+ Workspace
91
+ </div>
92
+ <div
93
+ style={{
94
+ ...cardStyles.base,
95
+ background: colors.backgroundHover,
96
+ }}
97
+ >
98
+ {workspace ? (
99
+ <div>
100
+ <div
101
+ style={{
102
+ fontSize: typography.sizes.base,
103
+ fontWeight: typography.weights.semibold,
104
+ color: colors.text,
105
+ }}
106
+ >
107
+ {workspace.name}
108
+ </div>
109
+ <div
110
+ style={{
111
+ fontSize: typography.sizes.xs,
112
+ color: colors.textMuted,
113
+ fontFamily: "monospace",
114
+ }}
115
+ >
116
+ ID: {workspace.id.slice(0, 8)}...
117
+ </div>
118
+ </div>
119
+ ) : (
120
+ <span style={{ color: colors.textMuted }}>Loading...</span>
121
+ )}
122
+ </div>
123
+ </div>
124
+
125
+ {/* User info */}
126
+ <div style={{ marginBottom: spacing.md }}>
127
+ <div
128
+ style={{
129
+ fontSize: typography.sizes.sm,
130
+ fontWeight: typography.weights.medium,
131
+ color: colors.textSecondary,
132
+ marginBottom: spacing.xs,
133
+ }}
134
+ >
135
+ Current User
136
+ </div>
137
+ <div
138
+ style={{
139
+ ...cardStyles.base,
140
+ background: colors.backgroundHover,
141
+ display: "flex",
142
+ alignItems: "center",
143
+ gap: spacing.sm,
144
+ }}
145
+ >
146
+ {user ? (
147
+ <>
148
+ <div
149
+ style={{
150
+ width: 36,
151
+ height: 36,
152
+ borderRadius: "50%",
153
+ background: colors.primaryLight,
154
+ display: "flex",
155
+ alignItems: "center",
156
+ justifyContent: "center",
157
+ overflow: "hidden",
158
+ }}
159
+ >
160
+ {user.avatarUrl ? (
161
+ <img
162
+ src={user.avatarUrl}
163
+ alt={user.displayName}
164
+ style={{ width: "100%", height: "100%", objectFit: "cover" }}
165
+ />
166
+ ) : (
167
+ <span style={{ fontSize: typography.sizes.lg }}>
168
+ {user.displayName.charAt(0).toUpperCase()}
169
+ </span>
170
+ )}
171
+ </div>
172
+ <div>
173
+ <div
174
+ style={{
175
+ fontSize: typography.sizes.base,
176
+ fontWeight: typography.weights.medium,
177
+ color: colors.text,
178
+ }}
179
+ >
180
+ {user.displayName}
181
+ </div>
182
+ <div
183
+ style={{
184
+ fontSize: typography.sizes.xs,
185
+ color: colors.textMuted,
186
+ fontFamily: "monospace",
187
+ }}
188
+ >
189
+ ID: {user.id.slice(0, 8)}...
190
+ </div>
191
+ </div>
192
+ </>
193
+ ) : (
194
+ <span style={{ color: colors.textMuted }}>Not signed in</span>
195
+ )}
196
+ </div>
197
+ </div>
198
+
199
+ {/* Theme info */}
200
+ <div style={{ marginBottom: spacing.md }}>
201
+ <div
202
+ style={{
203
+ fontSize: typography.sizes.sm,
204
+ fontWeight: typography.weights.medium,
205
+ color: colors.textSecondary,
206
+ marginBottom: spacing.xs,
207
+ }}
208
+ >
209
+ Theme
210
+ </div>
211
+ <div
212
+ style={{
213
+ ...cardStyles.base,
214
+ background: colors.backgroundHover,
215
+ }}
216
+ >
217
+ {theme ? (
218
+ <div>
219
+ <div
220
+ style={{
221
+ display: "flex",
222
+ alignItems: "center",
223
+ gap: spacing.sm,
224
+ marginBottom: spacing.sm,
225
+ }}
226
+ >
227
+ <span style={{ fontSize: typography.sizes.lg }}>
228
+ {theme.mode === "dark" ? "🌙" : "☀️"}
229
+ </span>
230
+ <span
231
+ style={{
232
+ fontSize: typography.sizes.base,
233
+ fontWeight: typography.weights.medium,
234
+ color: colors.text,
235
+ textTransform: "capitalize",
236
+ }}
237
+ >
238
+ {theme.mode} Mode
239
+ </span>
240
+ </div>
241
+ <div
242
+ style={{
243
+ display: "flex",
244
+ gap: spacing.xs,
245
+ flexWrap: "wrap",
246
+ }}
247
+ >
248
+ {Object.entries(theme.colors)
249
+ .slice(0, 6)
250
+ .map(([name, colorValue]) => (
251
+ <div
252
+ key={name}
253
+ title={`${name}: ${colorValue}`}
254
+ style={{
255
+ width: 24,
256
+ height: 24,
257
+ borderRadius: 4,
258
+ background: colorValue as string,
259
+ border: `1px solid ${colors.border}`,
260
+ }}
261
+ />
262
+ ))}
263
+ </div>
264
+ </div>
265
+ ) : (
266
+ <span style={{ color: colors.textMuted }}>Loading...</span>
267
+ )}
268
+ </div>
269
+ </div>
270
+
271
+ {/* Refresh button */}
272
+ <button
273
+ onClick={refreshAll}
274
+ onPointerDown={handlePointerDown}
275
+ style={{ ...buttonStyles.secondary, width: "100%" }}
276
+ >
277
+ ↻ Refresh All
278
+ </button>
279
+ </div>
280
+ );
281
+ }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * NetworkControls - Shared Network API controls
3
+ *
4
+ * Used by both NetworkStep (onboarding) and NetworkTab (playground).
5
+ * Demonstrates calling external APIs with real-world examples.
6
+ */
7
+ import React, { useState, useCallback } from "react";
8
+ import type { DOEExtensionAPI } from "doe-sdk/runtime";
9
+ import { useInteractivePointerDown } from "../../hooks/usePointerHandlers";
10
+ import { spacing, colors, typography, buttonStyles, inputStyles } from "../../styles";
11
+
12
+ export interface NetworkControlsProps {
13
+ api: DOEExtensionAPI;
14
+ /** Callback when result changes (for parent to display) */
15
+ onResult?: (result: string) => void;
16
+ }
17
+
18
+ type ApiExample = "joke" | "github";
19
+
20
+ interface ApiResult {
21
+ type: ApiExample;
22
+ data: JokeData | GitHubData | null;
23
+ }
24
+
25
+ interface JokeData {
26
+ joke: string;
27
+ }
28
+
29
+ interface GitHubData {
30
+ login: string;
31
+ name: string | null;
32
+ avatar_url: string;
33
+ public_repos: number;
34
+ followers: number;
35
+ }
36
+
37
+ export function NetworkControls({
38
+ api,
39
+ onResult,
40
+ }: NetworkControlsProps): React.ReactElement {
41
+ const [githubUsername, setGithubUsername] = useState("torvalds");
42
+ const [result, setApiResult] = useState<ApiResult | null>(null);
43
+ const [loading, setLoading] = useState<ApiExample | null>(null);
44
+ const handlePointerDown = useInteractivePointerDown();
45
+
46
+ const setResult = useCallback(
47
+ (msg: string) => {
48
+ onResult?.(msg);
49
+ },
50
+ [onResult]
51
+ );
52
+
53
+ const fetchJoke = useCallback(async () => {
54
+ setLoading("joke");
55
+ setApiResult(null);
56
+ try {
57
+ const response = await api.network.fetch(
58
+ "https://icanhazdadjoke.com/",
59
+ {
60
+ timeout: 10000,
61
+ headers: { Accept: "application/json" },
62
+ }
63
+ );
64
+
65
+ if (!response.ok) {
66
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
67
+ }
68
+
69
+ const data = await response.json<{ joke: string }>();
70
+ setApiResult({ type: "joke", data: { joke: data.joke } });
71
+ setResult("Fetched random joke from icanhazdadjoke.com");
72
+ } catch (error) {
73
+ const errorMsg = error instanceof Error ? error.message : "Unknown";
74
+ setResult(`Error: ${errorMsg}`);
75
+ } finally {
76
+ setLoading(null);
77
+ }
78
+ }, [api, setResult]);
79
+
80
+ const fetchGitHub = useCallback(async () => {
81
+ if (!githubUsername.trim()) {
82
+ setResult("Please enter a GitHub username");
83
+ return;
84
+ }
85
+ setLoading("github");
86
+ setApiResult(null);
87
+ try {
88
+ const response = await api.network.fetch(
89
+ `https://api.github.com/users/${encodeURIComponent(githubUsername.trim())}`,
90
+ { timeout: 10000 }
91
+ );
92
+
93
+ if (!response.ok) {
94
+ if (response.status === 404) {
95
+ throw new Error("User not found");
96
+ }
97
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
98
+ }
99
+
100
+ const data = await response.json<GitHubData>();
101
+ setApiResult({
102
+ type: "github",
103
+ data: {
104
+ login: data.login,
105
+ name: data.name,
106
+ avatar_url: data.avatar_url,
107
+ public_repos: data.public_repos,
108
+ followers: data.followers,
109
+ },
110
+ });
111
+ setResult(`Fetched GitHub profile: ${data.login}`);
112
+ } catch (error) {
113
+ const errorMsg = error instanceof Error ? error.message : "Unknown";
114
+ setResult(`Error: ${errorMsg}`);
115
+ } finally {
116
+ setLoading(null);
117
+ }
118
+ }, [api, githubUsername, setResult]);
119
+
120
+ return (
121
+ <div>
122
+ {/* API Examples Header */}
123
+ <div
124
+ style={{
125
+ fontSize: typography.sizes.sm,
126
+ color: colors.textSecondary,
127
+ marginBottom: spacing.sm,
128
+ }}
129
+ >
130
+ Try calling external APIs:
131
+ </div>
132
+
133
+ {/* Random Joke API */}
134
+ <div
135
+ style={{
136
+ background: colors.backgroundHover,
137
+ borderRadius: 8,
138
+ padding: spacing.md,
139
+ marginBottom: spacing.sm,
140
+ }}
141
+ >
142
+ <div
143
+ style={{
144
+ display: "flex",
145
+ alignItems: "center",
146
+ justifyContent: "space-between",
147
+ gap: spacing.sm,
148
+ }}
149
+ >
150
+ <div>
151
+ <div
152
+ style={{
153
+ fontSize: typography.sizes.sm,
154
+ fontWeight: typography.weights.medium,
155
+ color: colors.text,
156
+ }}
157
+ >
158
+ 🎭 Random Joke API
159
+ </div>
160
+ <div
161
+ style={{
162
+ fontSize: typography.sizes.xs,
163
+ color: colors.textMuted,
164
+ }}
165
+ >
166
+ icanhazdadjoke.com
167
+ </div>
168
+ </div>
169
+ <button
170
+ onClick={fetchJoke}
171
+ onPointerDown={handlePointerDown}
172
+ style={buttonStyles.primary}
173
+ disabled={loading !== null}
174
+ >
175
+ {loading === "joke" ? "..." : "Get Joke"}
176
+ </button>
177
+ </div>
178
+ </div>
179
+
180
+ {/* GitHub API */}
181
+ <div
182
+ style={{
183
+ background: colors.backgroundHover,
184
+ borderRadius: 8,
185
+ padding: spacing.md,
186
+ marginBottom: spacing.md,
187
+ }}
188
+ >
189
+ <div
190
+ style={{
191
+ fontSize: typography.sizes.sm,
192
+ fontWeight: typography.weights.medium,
193
+ color: colors.text,
194
+ marginBottom: spacing.xs,
195
+ }}
196
+ >
197
+ 🐙 GitHub User API
198
+ </div>
199
+ <div
200
+ style={{
201
+ fontSize: typography.sizes.xs,
202
+ color: colors.textMuted,
203
+ marginBottom: spacing.sm,
204
+ }}
205
+ >
206
+ api.github.com
207
+ </div>
208
+ <div style={{ display: "flex", gap: spacing.sm }}>
209
+ <input
210
+ type="text"
211
+ value={githubUsername}
212
+ onChange={(e) => setGithubUsername(e.target.value)}
213
+ onKeyDown={(e) => e.key === "Enter" && fetchGitHub()}
214
+ onPointerDown={handlePointerDown}
215
+ placeholder="GitHub username"
216
+ style={{ ...inputStyles.base, flex: 1 }}
217
+ />
218
+ <button
219
+ onClick={fetchGitHub}
220
+ onPointerDown={handlePointerDown}
221
+ style={buttonStyles.primary}
222
+ disabled={loading !== null}
223
+ >
224
+ {loading === "github" ? "..." : "Fetch"}
225
+ </button>
226
+ </div>
227
+ </div>
228
+
229
+ {/* Result display */}
230
+ {result?.data && (
231
+ <div
232
+ style={{
233
+ background: `linear-gradient(135deg, ${colors.primaryLight} 0%, #e0e7ff 100%)`,
234
+ borderRadius: 8,
235
+ padding: spacing.md,
236
+ animation: "fadeIn 0.3s ease-out",
237
+ }}
238
+ >
239
+ {result.type === "joke" && result.data && (
240
+ <div
241
+ style={{
242
+ fontSize: typography.sizes.base,
243
+ color: colors.text,
244
+ lineHeight: 1.5,
245
+ fontStyle: "italic",
246
+ }}
247
+ >
248
+ "{(result.data as JokeData).joke}"
249
+ </div>
250
+ )}
251
+ {result.type === "github" && result.data && (
252
+ <div
253
+ style={{
254
+ display: "flex",
255
+ alignItems: "center",
256
+ gap: spacing.md,
257
+ }}
258
+ >
259
+ <img
260
+ src={(result.data as GitHubData).avatar_url}
261
+ alt="avatar"
262
+ style={{
263
+ width: 48,
264
+ height: 48,
265
+ borderRadius: "50%",
266
+ border: `2px solid ${colors.primary}`,
267
+ }}
268
+ />
269
+ <div style={{ flex: 1 }}>
270
+ <div
271
+ style={{
272
+ fontSize: typography.sizes.base,
273
+ fontWeight: typography.weights.semibold,
274
+ color: colors.text,
275
+ }}
276
+ >
277
+ {(result.data as GitHubData).name || (result.data as GitHubData).login}
278
+ </div>
279
+ <div
280
+ style={{
281
+ fontSize: typography.sizes.xs,
282
+ color: colors.textSecondary,
283
+ }}
284
+ >
285
+ @{(result.data as GitHubData).login}
286
+ </div>
287
+ <div
288
+ style={{
289
+ display: "flex",
290
+ gap: spacing.md,
291
+ marginTop: spacing.xs,
292
+ fontSize: typography.sizes.xs,
293
+ color: colors.textMuted,
294
+ }}
295
+ >
296
+ <span>{(result.data as GitHubData).public_repos} repos</span>
297
+ <span>{(result.data as GitHubData).followers} followers</span>
298
+ </div>
299
+ </div>
300
+ </div>
301
+ )}
302
+ </div>
303
+ )}
304
+
305
+ {/* Host permissions note */}
306
+ <div
307
+ style={{
308
+ marginTop: spacing.md,
309
+ fontSize: typography.sizes.xs,
310
+ color: colors.textMuted,
311
+ }}
312
+ >
313
+ Note: Network requests are limited to domains listed in{" "}
314
+ <code style={{ background: colors.backgroundHover, padding: "2px 4px", borderRadius: 4 }}>
315
+ hostPermissions
316
+ </code>{" "}
317
+ in manifest.json
318
+ </div>
319
+
320
+ <style>{`
321
+ @keyframes fadeIn {
322
+ from { opacity: 0; }
323
+ to { opacity: 1; }
324
+ }
325
+ `}</style>
326
+ </div>
327
+ );
328
+ }