@xpufx/paseo-forges 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 (128) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +109 -0
  3. package/client/active-forge.ts +47 -0
  4. package/client/board-alert.tsx +236 -0
  5. package/client/foreign-link.tsx +33 -0
  6. package/client/hook-queue-panel.tsx +412 -0
  7. package/client/issues-pill.tsx +1976 -0
  8. package/client/label-chip.tsx +116 -0
  9. package/client/linkifier.tsx +280 -0
  10. package/client/pill-label.ts +183 -0
  11. package/client/vendor/paseo-plugin-helper/command-center.ts +43 -0
  12. package/client/vendor/paseo-plugin-helper/components/AboutSection.tsx +493 -0
  13. package/client/vendor/paseo-plugin-helper/components/AttentionBeacon.tsx +250 -0
  14. package/client/vendor/paseo-plugin-helper/components/Badge.tsx +156 -0
  15. package/client/vendor/paseo-plugin-helper/components/Button.tsx +178 -0
  16. package/client/vendor/paseo-plugin-helper/components/Card.tsx +225 -0
  17. package/client/vendor/paseo-plugin-helper/components/CodeBlock.tsx +196 -0
  18. package/client/vendor/paseo-plugin-helper/components/Collapsible.tsx +277 -0
  19. package/client/vendor/paseo-plugin-helper/components/CommandBox.tsx +172 -0
  20. package/client/vendor/paseo-plugin-helper/components/CopyButton.tsx +180 -0
  21. package/client/vendor/paseo-plugin-helper/components/DataTable.tsx +200 -0
  22. package/client/vendor/paseo-plugin-helper/components/EmptyState.tsx +97 -0
  23. package/client/vendor/paseo-plugin-helper/components/HighlightedText.tsx +70 -0
  24. package/client/vendor/paseo-plugin-helper/components/InlineButton.tsx +73 -0
  25. package/client/vendor/paseo-plugin-helper/components/KeyValue.tsx +446 -0
  26. package/client/vendor/paseo-plugin-helper/components/MetricGauge.tsx +247 -0
  27. package/client/vendor/paseo-plugin-helper/components/ProgressBar.tsx +117 -0
  28. package/client/vendor/paseo-plugin-helper/components/Responsive.tsx +53 -0
  29. package/client/vendor/paseo-plugin-helper/components/SearchInput.tsx +118 -0
  30. package/client/vendor/paseo-plugin-helper/components/SectionHeader.tsx +80 -0
  31. package/client/vendor/paseo-plugin-helper/components/Select.tsx +215 -0
  32. package/client/vendor/paseo-plugin-helper/components/StatusDot.tsx +80 -0
  33. package/client/vendor/paseo-plugin-helper/components/Tabs.tsx +319 -0
  34. package/client/vendor/paseo-plugin-helper/components/TextInput.tsx +150 -0
  35. package/client/vendor/paseo-plugin-helper/components/Toggle.tsx +163 -0
  36. package/client/vendor/paseo-plugin-helper/components/TruncatedText.tsx +157 -0
  37. package/client/vendor/paseo-plugin-helper/components/index.ts +25 -0
  38. package/client/vendor/paseo-plugin-helper/custom-pills.tsx +224 -0
  39. package/client/vendor/paseo-plugin-helper/forge-icon.tsx +79 -0
  40. package/client/vendor/paseo-plugin-helper/host.ts +277 -0
  41. package/client/vendor/paseo-plugin-helper/icon.tsx +39 -0
  42. package/client/vendor/paseo-plugin-helper/index.ts +28 -0
  43. package/client/vendor/paseo-plugin-helper/layout/ActionBar.tsx +49 -0
  44. package/client/vendor/paseo-plugin-helper/layout/FormRow.tsx +103 -0
  45. package/client/vendor/paseo-plugin-helper/layout/Grid.tsx +65 -0
  46. package/client/vendor/paseo-plugin-helper/layout/ModalBody.tsx +378 -0
  47. package/client/vendor/paseo-plugin-helper/layout/ModalContent.tsx +49 -0
  48. package/client/vendor/paseo-plugin-helper/layout/Row.tsx +39 -0
  49. package/client/vendor/paseo-plugin-helper/layout/Stack.tsx +39 -0
  50. package/client/vendor/paseo-plugin-helper/layout/index.ts +7 -0
  51. package/client/vendor/paseo-plugin-helper/panel.tsx +81 -0
  52. package/client/vendor/paseo-plugin-helper/pill.tsx +884 -0
  53. package/client/vendor/paseo-plugin-helper/query-refresh.ts +79 -0
  54. package/client/vendor/paseo-plugin-helper/query.ts +66 -0
  55. package/client/vendor/paseo-plugin-helper/settings-screen.tsx +372 -0
  56. package/client/vendor/paseo-plugin-helper/settings.ts +181 -0
  57. package/client/vendor/paseo-plugin-helper/shared-settings.ts +46 -0
  58. package/client/vendor/paseo-plugin-helper/snapshot.ts +68 -0
  59. package/client/vendor/paseo-plugin-helper/surface.tsx +80 -0
  60. package/client/vendor/paseo-plugin-helper/theme/color-utils.ts +118 -0
  61. package/client/vendor/paseo-plugin-helper/theme/flair.ts +76 -0
  62. package/client/vendor/paseo-plugin-helper/theme/host-variables.ts +121 -0
  63. package/client/vendor/paseo-plugin-helper/theme/index.ts +7 -0
  64. package/client/vendor/paseo-plugin-helper/theme/provider.tsx +214 -0
  65. package/client/vendor/paseo-plugin-helper/theme/responsive.ts +213 -0
  66. package/client/vendor/paseo-plugin-helper/theme/tokens.ts +161 -0
  67. package/client/vendor/paseo-plugin-helper/theme/useResponsive.ts +57 -0
  68. package/client/vendor/paseo-plugin-helper/utils/clipboard.ts +149 -0
  69. package/client/vendor/paseo-plugin-helper/utils/haptics.ts +34 -0
  70. package/client/webhook-card.tsx +198 -0
  71. package/docs/specs/forge-workflow-gui.md +820 -0
  72. package/docs/workflow.md +339 -0
  73. package/examples/README.md +108 -0
  74. package/examples/hook-service/README.md +66 -0
  75. package/examples/hook-service/forge-hook.service +28 -0
  76. package/examples/hook-service/hook-server.mjs +265 -0
  77. package/examples/hook-service/hook.env.example +21 -0
  78. package/examples/labels/README.md +64 -0
  79. package/examples/labels/label-base.yaml +117 -0
  80. package/examples/skills/coding-agent/SKILL.md +262 -0
  81. package/examples/skills/coding-agent-fgjx/SKILL.md +271 -0
  82. package/examples/skills/orchestrator/SKILL.md +133 -0
  83. package/examples/skills/orchestrator-fgjx/SKILL.md +139 -0
  84. package/examples/tools/README.md +68 -0
  85. package/examples/tools/fgjx +464 -0
  86. package/package.json +38 -0
  87. package/paseo-plugin.json +4 -0
  88. package/server/forge-client.ts +484 -0
  89. package/server/forge-guard.ts +70 -0
  90. package/server/git-origin.ts +70 -0
  91. package/server/hook-queue.ts +127 -0
  92. package/server/issues.ts +542 -0
  93. package/server/settings.ts +52 -0
  94. package/server/vendor/paseo-plugin-helper/agent.ts +85 -0
  95. package/server/vendor/paseo-plugin-helper/custom-pills.ts +344 -0
  96. package/server/vendor/paseo-plugin-helper/index.ts +18 -0
  97. package/server/vendor/paseo-plugin-helper/jsonc.ts +78 -0
  98. package/server/vendor/paseo-plugin-helper/logger.ts +210 -0
  99. package/server/vendor/paseo-plugin-helper/mcp-config.ts +367 -0
  100. package/server/vendor/paseo-plugin-helper/mcp-injection.ts +85 -0
  101. package/server/vendor/paseo-plugin-helper/network.ts +91 -0
  102. package/server/vendor/paseo-plugin-helper/plugins.ts +160 -0
  103. package/server/vendor/paseo-plugin-helper/process.ts +186 -0
  104. package/server/vendor/paseo-plugin-helper/redact.ts +86 -0
  105. package/server/vendor/paseo-plugin-helper/rpc-guard.ts +77 -0
  106. package/server/vendor/paseo-plugin-helper/settings.ts +97 -0
  107. package/server/vendor/paseo-plugin-helper/shared-settings.ts +243 -0
  108. package/server/vendor/paseo-plugin-helper/storage.ts +244 -0
  109. package/server/vendor/paseo-plugin-helper/system.ts +128 -0
  110. package/server/vendor/paseo-plugin-helper/task.ts +116 -0
  111. package/server/vendor/paseo-plugin-helper/version.ts +153 -0
  112. package/server/vendor/paseo-plugin-helper/workspace-beacon.ts +418 -0
  113. package/shared/hook-queue.ts +140 -0
  114. package/shared/issues.ts +1750 -0
  115. package/shared/vendor/paseo-plugin-helper/README.md +11 -0
  116. package/shared/vendor/paseo-plugin-helper/async.ts +35 -0
  117. package/shared/vendor/paseo-plugin-helper/custom-pills.ts +169 -0
  118. package/shared/vendor/paseo-plugin-helper/forge.ts +110 -0
  119. package/shared/vendor/paseo-plugin-helper/formatters.ts +271 -0
  120. package/shared/vendor/paseo-plugin-helper/highlight.ts +184 -0
  121. package/shared/vendor/paseo-plugin-helper/index.ts +10 -0
  122. package/shared/vendor/paseo-plugin-helper/rpc.ts +72 -0
  123. package/shared/vendor/paseo-plugin-helper/settings.ts +138 -0
  124. package/shared/vendor/paseo-plugin-helper/suite-settings.ts +17 -0
  125. package/shared/vendor/paseo-plugin-helper/suppressed.ts +31 -0
  126. package/shared/vendor/paseo-plugin-helper/types.ts +36 -0
  127. package/shared/version.ts +2 -0
  128. package/shared/webhook.ts +223 -0
@@ -0,0 +1,149 @@
1
+ import type { HostToast } from "../host";
2
+ import { getOptionalClientHost } from "../host";
3
+
4
+ export interface CopyToClipboardOptions {
5
+ toast?: HostToast;
6
+ toastMessage?: string;
7
+ }
8
+
9
+ export type ClipboardTier = "navigator" | "host" | "rnAsync" | "rnSync" | "execCommand";
10
+
11
+ export interface ClipboardEnvironment {
12
+ hasNavigatorClipboard: boolean;
13
+ hasHostCopyText: boolean;
14
+ hasRnClipboard: boolean;
15
+ hasRnSetStringAsync: boolean;
16
+ isDom: boolean;
17
+ }
18
+
19
+ /**
20
+ * Deterministic tier order for an explicit copy.
21
+ *
22
+ * Web's `navigator.clipboard.writeText` is the only API that rejects when the
23
+ * clipboard did not change, so it leads. The synchronous
24
+ * `react-native-web` `Clipboard.setString` reports success even when its
25
+ * `document.execCommand("copy")` fails, which leaves the previous clipboard
26
+ * item in place while the UI claims success (xpufx-org/paseo#278); it is only
27
+ * usable off-DOM (native), where it is the real platform clipboard. In a DOM
28
+ * the checked `execCommand` fallback is preferred to that unverifiable path.
29
+ */
30
+ export function clipboardTierOrder(env: ClipboardEnvironment): ClipboardTier[] {
31
+ const tiers: ClipboardTier[] = [];
32
+ if (env.hasNavigatorClipboard) tiers.push("navigator");
33
+ if (env.hasHostCopyText) tiers.push("host");
34
+ if (env.hasRnSetStringAsync) {
35
+ tiers.push("rnAsync");
36
+ } else if (env.hasRnClipboard && !env.isDom) {
37
+ tiers.push("rnSync");
38
+ }
39
+ tiers.push("execCommand");
40
+ return tiers;
41
+ }
42
+
43
+ /**
44
+ * Robust cross-platform clipboard copy helper for Paseo plugins.
45
+ * Works seamlessly across React Native (mobile), web, and desktop.
46
+ *
47
+ * Tier order comes from `clipboardTierOrder`; every tier reports failure
48
+ * honestly so a denied or blocked write never leaves the previous clipboard
49
+ * item behind under a fake success.
50
+ */
51
+ export async function copyToClipboard(
52
+ text: string,
53
+ options?: CopyToClipboardOptions,
54
+ ): Promise<boolean> {
55
+ if (text === null || text === undefined) return false;
56
+ const str = String(text);
57
+
58
+ const globalObj = typeof globalThis !== "undefined" ? (globalThis as any) : {};
59
+
60
+ let rn: any;
61
+ try {
62
+ rn = require("react-native");
63
+ } catch {
64
+ // Ignore require error if not in RN context
65
+ }
66
+ const rnClipboard = rn?.Clipboard;
67
+ const rnSetStringAsync =
68
+ typeof rnClipboard?.setStringAsync === "function" ? rnClipboard.setStringAsync : undefined;
69
+ const rnSetString =
70
+ typeof rnClipboard?.setString === "function" ? rnClipboard.setString : undefined;
71
+ const copyText = getOptionalClientHost()?.copyText;
72
+
73
+ const isDom =
74
+ typeof globalObj.document !== "undefined" &&
75
+ typeof globalObj.document?.createElement === "function";
76
+
77
+ const tiers = clipboardTierOrder({
78
+ hasNavigatorClipboard: Boolean(globalObj.navigator?.clipboard?.writeText),
79
+ hasHostCopyText: Boolean(copyText),
80
+ hasRnClipboard: Boolean(rnSetStringAsync ?? rnSetString),
81
+ hasRnSetStringAsync: Boolean(rnSetStringAsync),
82
+ isDom,
83
+ });
84
+
85
+ let success = false;
86
+ for (const tier of tiers) {
87
+ if (success) break;
88
+ try {
89
+ switch (tier) {
90
+ case "navigator":
91
+ await globalObj.navigator.clipboard.writeText(str);
92
+ success = true;
93
+ break;
94
+ case "host":
95
+ if (copyText) {
96
+ await copyText(str);
97
+ success = true;
98
+ }
99
+ break;
100
+ case "rnAsync":
101
+ // Expo's async API returns a boolean; a rejected promise falls through.
102
+ success = (await rnSetStringAsync(str)) !== false;
103
+ break;
104
+ case "rnSync":
105
+ // Only reached off-DOM, where this is the native platform clipboard.
106
+ rnSetString(str);
107
+ success = true;
108
+ break;
109
+ case "execCommand": {
110
+ const doc = globalObj.document;
111
+ if (doc?.createElement && doc?.body) {
112
+ const textarea = doc.createElement("textarea");
113
+ textarea.value = str;
114
+ textarea.style.position = "fixed";
115
+ textarea.style.opacity = "0";
116
+ textarea.style.left = "-9999px";
117
+ doc.body.appendChild(textarea);
118
+ textarea.focus();
119
+ textarea.select();
120
+ const res = doc.execCommand("copy");
121
+ doc.body.removeChild(textarea);
122
+ success = res === true;
123
+ }
124
+ break;
125
+ }
126
+ }
127
+ } catch {
128
+ // Tier failed; try the next one.
129
+ }
130
+ }
131
+
132
+ if (success && options?.toast) {
133
+ try {
134
+ const toastAny = options.toast as any;
135
+ if (typeof toastAny.copied === "function") {
136
+ toastAny.copied(options.toastMessage);
137
+ } else if (typeof toastAny.show === "function") {
138
+ const msg = options.toastMessage
139
+ ? `Copied ${options.toastMessage} to clipboard`
140
+ : "Copied to clipboard";
141
+ toastAny.show(msg, { variant: "success" });
142
+ }
143
+ } catch {
144
+ // Ignore toast failure
145
+ }
146
+ }
147
+
148
+ return success;
149
+ }
@@ -0,0 +1,34 @@
1
+ export type HapticFeedbackType =
2
+ | "light"
3
+ | "medium"
4
+ | "heavy"
5
+ | "success"
6
+ | "warning"
7
+ | "error";
8
+
9
+ const PATTERNS: Record<HapticFeedbackType, number | number[]> = {
10
+ light: 10,
11
+ medium: 25,
12
+ heavy: 45,
13
+ success: [15, 40, 20],
14
+ warning: [30, 50, 30],
15
+ error: [40, 60, 40, 60, 40],
16
+ };
17
+
18
+ /**
19
+ * Cross-platform haptic feedback helper for Paseo plugins.
20
+ * Supports web vibration API and graceful fallback when vibration is unavailable.
21
+ */
22
+ export function triggerHaptic(type: HapticFeedbackType = "light"): boolean {
23
+ try {
24
+ const globalObj = typeof globalThis !== "undefined" ? (globalThis as any) : {};
25
+ const nav = globalObj.navigator;
26
+ if (typeof nav?.vibrate === "function") {
27
+ const pattern = PATTERNS[type] ?? 15;
28
+ return Boolean(nav.vibrate(pattern));
29
+ }
30
+ } catch {
31
+ // Ignore environments where vibration permissions are denied or unavailable
32
+ }
33
+ return false;
34
+ }
@@ -0,0 +1,198 @@
1
+ import React from "react";
2
+ import { Linking, Pressable, StyleSheet, Text, View } from "react-native";
3
+ import type {
4
+ PluginTimelineItemProps,
5
+ PluginTimelineRendererContribution,
6
+ PluginTimelineTransformerContribution,
7
+ } from "@getpaseo/plugin/client";
8
+ import type { PluginTheme } from "@getpaseo/plugin";
9
+ import { Badge, ForgeIcon, Icon } from "paseo-plugin-helper/client";
10
+ import {
11
+ forgejoWebhookCardSchema,
12
+ forgejoWebhookItem,
13
+ subjectLinkUrl,
14
+ type ForgejoSubject,
15
+ type ForgejoWebhookCardData,
16
+ } from "../shared/webhook.js";
17
+
18
+ /**
19
+ * Forgejo webhook timeline transformer: incoming hook deliveries become typed
20
+ * `forgejo-webhook` items so the composer renders the event card below instead
21
+ * of a raw chat line. Registered before the linkifier because hook messages
22
+ * carry a bare issue URL the linkifier would otherwise claim first.
23
+ *
24
+ * Presentation-only: `PluginTimelineTransformerContribution.transform` returns
25
+ * `PluginTimelineItem[]`, a client-side render target. It has no reference to
26
+ * mutate the source `AgentTimelineItem`, so the raw envelope + human line stays
27
+ * verbatim in the agent's context. Nothing below may echo that text.
28
+ */
29
+ export const forgejoWebhookUserTransformer: PluginTimelineTransformerContribution<"user_message"> = {
30
+ id: "forgejo-webhook",
31
+ query: { itemType: "user_message" },
32
+ transform({ item }) {
33
+ return forgejoWebhookItem(item.text) as any;
34
+ },
35
+ };
36
+
37
+ function openUrl(url: string) {
38
+ Linking.openURL(url).catch(() => {});
39
+ }
40
+
41
+ function SubjectRow({ theme, subject }: { theme: PluginTheme; subject: ForgejoSubject }) {
42
+ const link = subjectLinkUrl(subject);
43
+ const onPress = link ? () => openUrl(link) : undefined;
44
+ const isPush = subject.kind === "push";
45
+ return (
46
+ <View style={styles.subjectRow}>
47
+ <Icon
48
+ name={isPush ? "GitCommit" : "GitPullRequest"}
49
+ size={13}
50
+ color={theme.colors.foregroundMuted}
51
+ />
52
+ {subject.number != null ? <Badge variant="info" label={`#${subject.number}`} /> : null}
53
+ <View style={styles.subjectBody}>
54
+ {subject.title ? (
55
+ onPress ? (
56
+ <Pressable onPress={onPress} hitSlop={8} accessibilityRole="link">
57
+ <Text style={[styles.subjectTitle, styles.link, { color: theme.colors.accent }]}>
58
+ {subject.title}
59
+ </Text>
60
+ </Pressable>
61
+ ) : (
62
+ <Text style={[styles.subjectTitle, { color: theme.colors.foreground }]}>
63
+ {subject.title}
64
+ </Text>
65
+ )
66
+ ) : null}
67
+ {subject.ref ? (
68
+ <Text style={[styles.subjectMeta, { color: theme.colors.foregroundMuted }]}>
69
+ {subject.ref}
70
+ </Text>
71
+ ) : null}
72
+ {subject.commits != null ? (
73
+ <Text style={[styles.subjectMeta, { color: theme.colors.foregroundMuted }]}>
74
+ {subject.commits} {subject.commits === 1 ? "commit" : "commits"}
75
+ </Text>
76
+ ) : null}
77
+ {subject.commentId != null ? (
78
+ <Text style={[styles.subjectMeta, { color: theme.colors.foregroundMuted }]}>
79
+ comment #{subject.commentId}
80
+ </Text>
81
+ ) : null}
82
+ {link ? (
83
+ <Pressable onPress={onPress} hitSlop={8} accessibilityRole="link">
84
+ <Text style={[styles.subjectUrl, styles.link, { color: theme.colors.accent }]}>
85
+ {link}
86
+ </Text>
87
+ </Pressable>
88
+ ) : null}
89
+ </View>
90
+ </View>
91
+ );
92
+ }
93
+
94
+ export function ForgejoWebhookCard({ item, theme }: PluginTimelineItemProps<ForgejoWebhookCardData>) {
95
+ const data = item.data;
96
+ const repoLink = data.repoUrl ? () => openUrl(data.repoUrl as string) : undefined;
97
+ return (
98
+ <View
99
+ style={[
100
+ styles.card,
101
+ { backgroundColor: theme.colors.surface1, borderColor: theme.colors.border },
102
+ ]}
103
+ >
104
+ <View style={styles.header}>
105
+ <ForgeIcon host={data.repoUrl} size={14} color={theme.colors.accent} />
106
+ <Text style={[styles.title, { color: theme.colors.foreground }]}>Forgejo webhook</Text>
107
+ <Badge
108
+ variant="info"
109
+ label={data.action ? `${data.event}:${data.action}` : data.event}
110
+ />
111
+ </View>
112
+ <View style={styles.metaRow}>
113
+ {repoLink ? (
114
+ <Pressable onPress={repoLink} hitSlop={8} accessibilityRole="link">
115
+ <Text style={[styles.meta, styles.link, { color: theme.colors.accent }]}>
116
+ {data.repo}
117
+ </Text>
118
+ </Pressable>
119
+ ) : (
120
+ <Text style={[styles.meta, { color: theme.colors.foregroundMuted }]}>{data.repo}</Text>
121
+ )}
122
+ {data.sender ? (
123
+ <Text style={[styles.meta, { color: theme.colors.foregroundMuted }]}>
124
+ · by {data.sender}
125
+ </Text>
126
+ ) : null}
127
+ </View>
128
+ {data.subject ? <SubjectRow theme={theme} subject={data.subject} /> : null}
129
+ <Text style={[styles.footer, { color: theme.colors.foregroundMuted }]}>
130
+ via forges
131
+ </Text>
132
+ </View>
133
+ );
134
+ }
135
+
136
+ export const forgejoWebhookRenderer: PluginTimelineRendererContribution<
137
+ typeof forgejoWebhookCardSchema
138
+ > = {
139
+ kind: "forgejo-webhook",
140
+ version: 1,
141
+ schema: forgejoWebhookCardSchema,
142
+ Component: ForgejoWebhookCard,
143
+ };
144
+
145
+ const styles = StyleSheet.create({
146
+ card: {
147
+ borderRadius: 8,
148
+ borderWidth: 1,
149
+ padding: 8,
150
+ marginVertical: 2,
151
+ gap: 6,
152
+ },
153
+ header: {
154
+ flexDirection: "row",
155
+ alignItems: "center",
156
+ gap: 6,
157
+ },
158
+ title: {
159
+ fontSize: 12,
160
+ fontWeight: "600",
161
+ flex: 1,
162
+ },
163
+ metaRow: {
164
+ flexDirection: "row",
165
+ alignItems: "center",
166
+ flexWrap: "wrap",
167
+ gap: 4,
168
+ },
169
+ meta: {
170
+ fontSize: 11,
171
+ },
172
+ subjectRow: {
173
+ flexDirection: "row",
174
+ alignItems: "flex-start",
175
+ gap: 8,
176
+ },
177
+ subjectBody: {
178
+ flex: 1,
179
+ gap: 2,
180
+ },
181
+ subjectTitle: {
182
+ fontSize: 13,
183
+ fontWeight: "600",
184
+ },
185
+ subjectMeta: {
186
+ fontSize: 11,
187
+ },
188
+ subjectUrl: {
189
+ fontSize: 11,
190
+ },
191
+ link: {
192
+ textDecorationLine: "underline",
193
+ },
194
+ footer: {
195
+ fontSize: 10,
196
+ fontStyle: "italic",
197
+ },
198
+ });