@ketrics/ketrics-cli 0.2.3 → 0.4.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 (35) hide show
  1. package/README.md +623 -607
  2. package/dist/src/version.d.ts +1 -1
  3. package/dist/src/version.js +1 -1
  4. package/package.json +1 -1
  5. package/templates/HelloWorld/README.md +83 -106
  6. package/templates/HelloWorld/backend/package.json +1 -1
  7. package/templates/HelloWorld/backend/src/database.ts +108 -0
  8. package/templates/HelloWorld/backend/src/excel.ts +118 -0
  9. package/templates/HelloWorld/backend/src/http.ts +22 -0
  10. package/templates/HelloWorld/backend/src/index.ts +105 -29
  11. package/templates/HelloWorld/backend/src/jobs.ts +47 -0
  12. package/templates/HelloWorld/backend/src/messages.ts +59 -0
  13. package/templates/HelloWorld/backend/src/pdf.ts +212 -0
  14. package/templates/HelloWorld/backend/src/secrets.ts +21 -14
  15. package/templates/HelloWorld/backend/src/volumes.ts +107 -0
  16. package/templates/HelloWorld/frontend/package.json +1 -3
  17. package/templates/HelloWorld/frontend/src/App.css +62 -37
  18. package/templates/HelloWorld/frontend/src/App.tsx +131 -111
  19. package/templates/HelloWorld/frontend/src/mocks/handlers.ts +149 -0
  20. package/templates/HelloWorld/frontend/src/mocks/mock-client.ts +45 -0
  21. package/templates/HelloWorld/frontend/src/services/index.ts +38 -20
  22. package/templates/HelloWorld/frontend/src/vite-env.d.ts +1 -0
  23. package/templates/HelloWorld/tests/test.createInvoicePdf.json +18 -0
  24. package/templates/HelloWorld/tests/{test.getSecretWithoutGrant.json → test.createSimplePdf.json} +4 -2
  25. package/templates/HelloWorld/tests/test.createSpreadsheet.json +11 -0
  26. package/templates/HelloWorld/tests/test.echo.json +2 -2
  27. package/templates/HelloWorld/tests/test.fetchExternalApi.json +13 -0
  28. package/templates/HelloWorld/tests/test.getSecret.json +5 -1
  29. package/templates/HelloWorld/tests/test.info.json +3 -1
  30. package/templates/HelloWorld/tests/test.listFiles.json +13 -0
  31. package/templates/HelloWorld/tests/{test.echo2.json → test.queryUsers.json} +3 -3
  32. package/templates/HelloWorld/tests/{test.greet.json → test.readFile.json} +2 -2
  33. package/templates/HelloWorld/tests/{test.testWriteFileWithoutVolumeGrant.json → test.saveFile.json} +4 -2
  34. package/templates/HelloWorld/tests/test.sendNotification.json +14 -0
  35. package/templates/HelloWorld/backend/src/volume.ts +0 -55
@@ -0,0 +1,47 @@
1
+ /**
2
+ * Background Job Examples
3
+ *
4
+ * Demonstrates scheduling and monitoring background jobs using ketrics.Job.
5
+ * Background jobs run asynchronously and can have longer timeouts (up to 15 minutes).
6
+ */
7
+
8
+ /**
9
+ * Schedule a function to run in the background.
10
+ */
11
+ const scheduleBackgroundJob = async (payload: {
12
+ functionName?: string;
13
+ data?: Record<string, unknown>;
14
+ }) => {
15
+ const jobId = await ketrics.Job.runInBackground({
16
+ function: payload?.functionName || "echo",
17
+ payload: payload?.data || { source: "background-job" },
18
+ options: {
19
+ timeout: 60000, // 1 minute timeout
20
+ },
21
+ });
22
+
23
+ return { jobId, status: "scheduled" };
24
+ };
25
+
26
+ /**
27
+ * Check the status of a background job.
28
+ */
29
+ const getJobStatus = async (payload: { jobId: string }) => {
30
+ if (!payload?.jobId) {
31
+ throw new Error("jobId is required");
32
+ }
33
+
34
+ const status = await ketrics.Job.getStatus(payload.jobId);
35
+
36
+ return {
37
+ jobId: status.jobId,
38
+ functionName: status.functionName,
39
+ status: status.status,
40
+ createdAt: status.createdAt,
41
+ startedAt: status.startedAt,
42
+ completedAt: status.completedAt,
43
+ error: status.error,
44
+ };
45
+ };
46
+
47
+ export { scheduleBackgroundJob, getJobStatus };
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Messages Examples
3
+ *
4
+ * Demonstrates sending notifications to users via ketrics.Messages.
5
+ * Messages appear in the user's inbox within the Ketrics portal.
6
+ */
7
+
8
+ /**
9
+ * Send a notification to the current user.
10
+ */
11
+ const sendNotification = async (payload: { subject?: string; body?: string }) => {
12
+ if (ketrics.requestor.type !== "USER") {
13
+ throw new Error("This function can only be called by a user");
14
+ }
15
+
16
+ const result = await ketrics.Messages.send({
17
+ userId: ketrics.requestor.userId!,
18
+ type: "notification",
19
+ subject: payload?.subject || "Hello from your app!",
20
+ body: payload?.body || `This notification was sent by **${ketrics.application.name}**.`,
21
+ priority: "MEDIUM",
22
+ channels: { inbox: true, push: false },
23
+ });
24
+
25
+ return {
26
+ messageId: result.messageId,
27
+ status: result.status,
28
+ };
29
+ };
30
+
31
+ /**
32
+ * Send a notification to multiple users.
33
+ */
34
+ const sendBulkNotification = async (payload: {
35
+ userIds: string[];
36
+ subject: string;
37
+ body: string;
38
+ }) => {
39
+ if (!payload?.userIds?.length) {
40
+ throw new Error("userIds array is required");
41
+ }
42
+
43
+ const result = await ketrics.Messages.sendBulk({
44
+ userIds: payload.userIds,
45
+ type: "announcement",
46
+ subject: payload.subject,
47
+ body: payload.body,
48
+ priority: "HIGH",
49
+ channels: { inbox: true, push: true },
50
+ });
51
+
52
+ return {
53
+ total: result.total,
54
+ sent: result.sent,
55
+ failed: result.failed,
56
+ };
57
+ };
58
+
59
+ export { sendNotification, sendBulkNotification };
@@ -0,0 +1,212 @@
1
+ /**
2
+ * PDF Examples
3
+ *
4
+ * Demonstrates PDF document creation using ketrics.Pdf.
5
+ * Generated PDFs can be saved to a volume for download.
6
+ */
7
+
8
+ /**
9
+ * Create a simple PDF document and save it to a volume.
10
+ */
11
+ const createSimplePdf = async () => {
12
+ const doc = await ketrics.Pdf.create();
13
+
14
+ // Set document metadata
15
+ doc.setTitle("Sample Document");
16
+ doc.setAuthor(ketrics.tenant.name);
17
+ doc.setCreator(ketrics.application.name);
18
+
19
+ // Add a page
20
+ const page = doc.addPage({ size: "A4" });
21
+
22
+ // Embed a standard font
23
+ const font = await doc.embedStandardFont("Helvetica");
24
+ const boldFont = await doc.embedStandardFont("Helvetica-Bold");
25
+
26
+ // Draw title
27
+ page.drawText("Hello from Ketrics!", {
28
+ x: 50,
29
+ y: 780,
30
+ size: 28,
31
+ font: boldFont,
32
+ color: ketrics.Pdf.rgb(0.2, 0.2, 0.6),
33
+ });
34
+
35
+ // Draw subtitle
36
+ page.drawText(`Generated by ${ketrics.application.name} for ${ketrics.tenant.name}`, {
37
+ x: 50,
38
+ y: 750,
39
+ size: 12,
40
+ font,
41
+ color: ketrics.Pdf.rgb(0.4, 0.4, 0.4),
42
+ });
43
+
44
+ // Draw a decorative line
45
+ page.drawLine({
46
+ start: { x: 50, y: 740 },
47
+ end: { x: 545, y: 740 },
48
+ thickness: 2,
49
+ color: ketrics.Pdf.rgb(0.2, 0.2, 0.6),
50
+ });
51
+
52
+ // Draw body text
53
+ page.drawText("This PDF was generated dynamically using the Ketrics PDF SDK.", {
54
+ x: 50,
55
+ y: 710,
56
+ size: 14,
57
+ font,
58
+ color: ketrics.Pdf.rgb(0, 0, 0),
59
+ });
60
+
61
+ page.drawText(`Date: ${new Date().toISOString()}`, {
62
+ x: 50,
63
+ y: 685,
64
+ size: 11,
65
+ font,
66
+ color: ketrics.Pdf.rgb(0.3, 0.3, 0.3),
67
+ });
68
+
69
+ // Save to volume
70
+ const buffer = await doc.toBuffer();
71
+ const volume = await ketrics.Volume.connect("test-volume");
72
+ const result = await volume.put("documents/sample.pdf", buffer, {
73
+ contentType: "application/pdf",
74
+ });
75
+
76
+ // Generate download URL
77
+ const downloadUrl = await volume.generateDownloadUrl("documents/sample.pdf", {
78
+ expiresIn: 3600,
79
+ });
80
+
81
+ return {
82
+ file: { key: result.key, size: result.size },
83
+ downloadUrl: downloadUrl.url,
84
+ pageCount: doc.getPageCount(),
85
+ };
86
+ };
87
+
88
+ /**
89
+ * Create an invoice-style PDF with structured data.
90
+ */
91
+ const createInvoicePdf = async (payload: {
92
+ invoiceNumber?: string;
93
+ customerName?: string;
94
+ items?: Array<{ description: string; quantity: number; price: number }>;
95
+ }) => {
96
+ const invoiceNumber = payload?.invoiceNumber || "INV-001";
97
+ const customerName = payload?.customerName || "Sample Customer";
98
+ const items = payload?.items || [
99
+ { description: "Web Development", quantity: 40, price: 150 },
100
+ { description: "Design Services", quantity: 20, price: 120 },
101
+ { description: "Consulting", quantity: 10, price: 200 },
102
+ ];
103
+
104
+ const doc = await ketrics.Pdf.create();
105
+ doc.setTitle(`Invoice ${invoiceNumber}`);
106
+ doc.setAuthor(ketrics.tenant.name);
107
+
108
+ const page = doc.addPage({ size: "A4" });
109
+ const font = await doc.embedStandardFont("Helvetica");
110
+ const boldFont = await doc.embedStandardFont("Helvetica-Bold");
111
+
112
+ let y = 780;
113
+
114
+ // Header
115
+ page.drawText("INVOICE", {
116
+ x: 50, y, size: 32, font: boldFont,
117
+ color: ketrics.Pdf.rgb(0.2, 0.2, 0.6),
118
+ });
119
+
120
+ y -= 35;
121
+ page.drawText(`#${invoiceNumber}`, {
122
+ x: 50, y, size: 14, font,
123
+ color: ketrics.Pdf.rgb(0.4, 0.4, 0.4),
124
+ });
125
+
126
+ // Company info (right side)
127
+ page.drawText(ketrics.tenant.name, {
128
+ x: 400, y: 780, size: 14, font: boldFont,
129
+ color: ketrics.Pdf.rgb(0, 0, 0),
130
+ });
131
+
132
+ page.drawText(new Date().toLocaleDateString(), {
133
+ x: 400, y: 762, size: 11, font,
134
+ color: ketrics.Pdf.rgb(0.4, 0.4, 0.4),
135
+ });
136
+
137
+ // Divider
138
+ y -= 20;
139
+ page.drawLine({
140
+ start: { x: 50, y },
141
+ end: { x: 545, y },
142
+ thickness: 1,
143
+ color: ketrics.Pdf.rgb(0.8, 0.8, 0.8),
144
+ });
145
+
146
+ // Bill To
147
+ y -= 30;
148
+ page.drawText("Bill To:", { x: 50, y, size: 10, font, color: ketrics.Pdf.rgb(0.5, 0.5, 0.5) });
149
+ y -= 18;
150
+ page.drawText(customerName, { x: 50, y, size: 14, font: boldFont, color: ketrics.Pdf.rgb(0, 0, 0) });
151
+
152
+ // Table header
153
+ y -= 40;
154
+ page.drawRectangle({
155
+ x: 50, y: y - 5, width: 495, height: 25,
156
+ color: ketrics.Pdf.rgb(0.93, 0.93, 0.97),
157
+ });
158
+
159
+ page.drawText("Description", { x: 60, y, size: 10, font: boldFont, color: ketrics.Pdf.rgb(0.3, 0.3, 0.3) });
160
+ page.drawText("Qty", { x: 330, y, size: 10, font: boldFont, color: ketrics.Pdf.rgb(0.3, 0.3, 0.3) });
161
+ page.drawText("Price", { x: 400, y, size: 10, font: boldFont, color: ketrics.Pdf.rgb(0.3, 0.3, 0.3) });
162
+ page.drawText("Total", { x: 480, y, size: 10, font: boldFont, color: ketrics.Pdf.rgb(0.3, 0.3, 0.3) });
163
+
164
+ // Table rows
165
+ let grandTotal = 0;
166
+ for (const item of items) {
167
+ y -= 25;
168
+ const lineTotal = item.quantity * item.price;
169
+ grandTotal += lineTotal;
170
+
171
+ page.drawText(item.description, { x: 60, y, size: 11, font, color: ketrics.Pdf.rgb(0, 0, 0) });
172
+ page.drawText(String(item.quantity), { x: 335, y, size: 11, font, color: ketrics.Pdf.rgb(0, 0, 0) });
173
+ page.drawText(`$${item.price.toFixed(2)}`, { x: 395, y, size: 11, font, color: ketrics.Pdf.rgb(0, 0, 0) });
174
+ page.drawText(`$${lineTotal.toFixed(2)}`, { x: 475, y, size: 11, font, color: ketrics.Pdf.rgb(0, 0, 0) });
175
+ }
176
+
177
+ // Total
178
+ y -= 15;
179
+ page.drawLine({
180
+ start: { x: 380, y },
181
+ end: { x: 545, y },
182
+ thickness: 1,
183
+ color: ketrics.Pdf.rgb(0.8, 0.8, 0.8),
184
+ });
185
+
186
+ y -= 20;
187
+ page.drawText("Total:", { x: 400, y, size: 14, font: boldFont, color: ketrics.Pdf.rgb(0, 0, 0) });
188
+ page.drawText(`$${grandTotal.toFixed(2)}`, {
189
+ x: 470, y, size: 14, font: boldFont,
190
+ color: ketrics.Pdf.rgb(0.2, 0.2, 0.6),
191
+ });
192
+
193
+ // Save to volume
194
+ const buffer = await doc.toBuffer();
195
+ const volume = await ketrics.Volume.connect("test-volume");
196
+ const fileName = `invoices/${invoiceNumber}.pdf`;
197
+ const result = await volume.put(fileName, buffer, {
198
+ contentType: "application/pdf",
199
+ });
200
+
201
+ const downloadUrl = await volume.generateDownloadUrl(fileName, { expiresIn: 3600 });
202
+
203
+ return {
204
+ file: { key: result.key, size: result.size },
205
+ downloadUrl: downloadUrl.url,
206
+ invoiceNumber,
207
+ total: grandTotal,
208
+ itemCount: items.length,
209
+ };
210
+ };
211
+
212
+ export { createSimplePdf, createInvoicePdf };
@@ -1,17 +1,24 @@
1
- const getSecret = async (key: string): Promise<{ value: string }> => {
2
- const value = await ketrics.Secret.get("apikey");
3
- if (!value) {
4
- throw new Error(`Secret with key "${key}" not found`);
5
- }
6
- return { value };
7
- };
1
+ /**
2
+ * Secret Examples
3
+ *
4
+ * Demonstrates encrypted secret retrieval using ketrics.Secret.
5
+ * Secrets must be created and granted to the application in the Ketrics portal.
6
+ */
7
+
8
+ /**
9
+ * Retrieve an encrypted secret by code.
10
+ */
11
+ const getSecret = async (payload: { code?: string }) => {
12
+ const secretCode = payload?.code || "apikey";
13
+ const value = await ketrics.Secret.get(secretCode);
8
14
 
9
- const getSecretWithoutGrant = async (key: string): Promise<{ value: string }> => {
10
- const value = await ketrics.Secret.get("missing-grant-secret");
11
- if (!value) {
12
- throw new Error(`Secret with key "${key}" not found`);
13
- }
14
- return { value };
15
+ // In production, never return the secret value directly.
16
+ // This is only for demonstration purposes.
17
+ return {
18
+ code: secretCode,
19
+ retrieved: true,
20
+ valueLength: value.length,
21
+ };
15
22
  };
16
23
 
17
- export { getSecret, getSecretWithoutGrant };
24
+ export { getSecret };
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Volume Examples
3
+ *
4
+ * Demonstrates S3-backed file storage operations using ketrics.Volume.
5
+ * Volumes must be granted to the application in the Ketrics portal before use.
6
+ */
7
+
8
+ /**
9
+ * Save files to a volume - demonstrates writing JSON and binary data.
10
+ */
11
+ const saveFile = async () => {
12
+ const volume = await ketrics.Volume.connect("test-volume");
13
+
14
+ // Write JSON content
15
+ const jsonData = {
16
+ generatedBy: ketrics.application.code,
17
+ generatedAt: new Date().toISOString(),
18
+ tenant: ketrics.tenant.code,
19
+ };
20
+ const jsonResult = await volume.put("output/data.json", JSON.stringify(jsonData), {
21
+ contentType: "application/json",
22
+ });
23
+
24
+ // Write plain text content
25
+ const textResult = await volume.put("output/hello.txt", Buffer.from("Hello from Ketrics!"), {
26
+ contentType: "text/plain",
27
+ });
28
+
29
+ return {
30
+ jsonFile: { key: jsonResult.key, etag: jsonResult.etag, size: jsonResult.size },
31
+ textFile: { key: textResult.key, etag: textResult.etag, size: textResult.size },
32
+ };
33
+ };
34
+
35
+ /**
36
+ * Read a file from a volume - demonstrates reading and parsing stored data.
37
+ */
38
+ const readFile = async () => {
39
+ const volume = await ketrics.Volume.connect("test-volume");
40
+
41
+ const exists = await volume.exists("output/data.json");
42
+ if (!exists) {
43
+ return { error: "File not found. Run saveFile first." };
44
+ }
45
+
46
+ const file = await volume.get("output/data.json");
47
+ const content = file.content.toString();
48
+
49
+ return {
50
+ contentType: file.contentType,
51
+ contentLength: file.contentLength,
52
+ lastModified: file.lastModified,
53
+ parsed: JSON.parse(content),
54
+ };
55
+ };
56
+
57
+ /**
58
+ * List files in a volume - demonstrates pagination and prefix filtering.
59
+ */
60
+ const listFiles = async (payload: { prefix?: string }) => {
61
+ const volume = await ketrics.Volume.connect("test-volume");
62
+
63
+ const result = await volume.list({
64
+ prefix: payload?.prefix || "output/",
65
+ maxResults: 50,
66
+ });
67
+
68
+ return {
69
+ files: result.files.map((f) => ({
70
+ key: f.key,
71
+ size: f.size,
72
+ lastModified: f.lastModified,
73
+ contentType: f.contentType,
74
+ })),
75
+ count: result.count,
76
+ isTruncated: result.isTruncated,
77
+ };
78
+ };
79
+
80
+ /**
81
+ * Generate a temporary download URL for a file.
82
+ */
83
+ const generateDownloadUrl = async () => {
84
+ const volume = await ketrics.Volume.connect("test-volume");
85
+ const result = await volume.generateDownloadUrl("output/data.json", {
86
+ expiresIn: 3600, // 1 hour
87
+ });
88
+
89
+ return { url: result.url, expiresAt: result.expiresAt };
90
+ };
91
+
92
+ /**
93
+ * Copy a file within a volume.
94
+ */
95
+ const copyFile = async () => {
96
+ const volume = await ketrics.Volume.connect("test-volume");
97
+
98
+ const result = await volume.copy("output/data.json", "backup/data-backup.json");
99
+
100
+ return {
101
+ sourceKey: result.sourceKey,
102
+ destinationKey: result.destinationKey,
103
+ etag: result.etag,
104
+ };
105
+ };
106
+
107
+ export { saveFile, readFile, listFiles, generateDownloadUrl, copyFile };
@@ -9,10 +9,8 @@
9
9
  },
10
10
  "dependencies": {
11
11
  "@ketrics/sdk-frontend": "^0.1.1",
12
- "axios": "^1.13.2",
13
12
  "react": "^18.2.0",
14
- "react-dom": "^18.2.0",
15
- "zod": "^3.22.4"
13
+ "react-dom": "^18.2.0"
16
14
  },
17
15
  "devDependencies": {
18
16
  "@types/react": "^18.2.0",
@@ -11,23 +11,24 @@ body {
11
11
  min-height: 100vh;
12
12
  display: flex;
13
13
  justify-content: center;
14
- align-items: center;
14
+ align-items: flex-start;
15
15
  padding: 20px;
16
16
  }
17
17
 
18
18
  .app {
19
19
  background: white;
20
20
  border-radius: 12px;
21
- padding: 40px;
22
- max-width: 500px;
21
+ padding: 32px;
22
+ max-width: 700px;
23
23
  width: 100%;
24
24
  box-shadow: 0 20px 60px rgba(0, 0, 0, 0.3);
25
+ margin-top: 20px;
25
26
  }
26
27
 
27
28
  h1 {
28
29
  color: #333;
29
- margin-bottom: 8px;
30
- font-size: 28px;
30
+ margin-bottom: 4px;
31
+ font-size: 26px;
31
32
  }
32
33
 
33
34
  .subtitle {
@@ -36,86 +37,110 @@ h1 {
36
37
  font-size: 14px;
37
38
  }
38
39
 
39
- .form {
40
+ .sections {
40
41
  display: flex;
41
- gap: 12px;
42
- margin-bottom: 16px;
42
+ flex-direction: column;
43
+ gap: 20px;
43
44
  }
44
45
 
45
- .input {
46
- flex: 1;
47
- padding: 12px 16px;
48
- border: 2px solid #e0e0e0;
49
- border-radius: 8px;
50
- font-size: 16px;
51
- transition: border-color 0.2s;
46
+ .section h2 {
47
+ font-size: 14px;
48
+ color: #555;
49
+ margin-bottom: 8px;
50
+ text-transform: uppercase;
51
+ letter-spacing: 0.5px;
52
52
  }
53
53
 
54
- .input:focus {
55
- outline: none;
56
- border-color: #667eea;
54
+ .button-group {
55
+ display: flex;
56
+ flex-wrap: wrap;
57
+ gap: 8px;
57
58
  }
58
59
 
59
60
  .button {
60
- padding: 12px 24px;
61
+ padding: 8px 16px;
61
62
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
62
63
  color: white;
63
64
  border: none;
64
- border-radius: 8px;
65
- font-size: 16px;
66
- font-weight: 600;
65
+ border-radius: 6px;
66
+ font-size: 13px;
67
+ font-weight: 500;
67
68
  cursor: pointer;
68
- transition: transform 0.2s, box-shadow 0.2s;
69
+ transition: transform 0.15s, box-shadow 0.15s, opacity 0.15s;
69
70
  }
70
71
 
71
- .button:hover {
72
- transform: translateY(-2px);
72
+ .button:hover:not(:disabled) {
73
+ transform: translateY(-1px);
73
74
  box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
74
75
  }
75
76
 
76
- .button:active {
77
+ .button:active:not(:disabled) {
77
78
  transform: translateY(0);
78
79
  }
79
80
 
81
+ .button:disabled {
82
+ opacity: 0.6;
83
+ cursor: not-allowed;
84
+ }
85
+
86
+ .loading {
87
+ color: #667eea;
88
+ font-weight: 500;
89
+ padding: 12px;
90
+ margin-top: 16px;
91
+ }
92
+
80
93
  .error {
81
94
  color: #e53e3e;
82
95
  background: #fed7d7;
83
96
  padding: 12px;
84
97
  border-radius: 8px;
85
- margin-bottom: 16px;
98
+ margin-top: 16px;
99
+ font-size: 14px;
86
100
  }
87
101
 
88
102
  .result {
89
- color: #38a169;
90
- background: #c6f6d5;
91
- padding: 12px;
103
+ background: #f0fdf4;
104
+ border: 1px solid #bbf7d0;
92
105
  border-radius: 8px;
93
- margin-bottom: 16px;
106
+ margin-top: 16px;
107
+ padding: 12px;
108
+ overflow-x: auto;
109
+ }
110
+
111
+ .result pre {
112
+ font-size: 12px;
113
+ color: #166534;
114
+ white-space: pre-wrap;
115
+ word-break: break-word;
116
+ margin: 0;
94
117
  }
95
118
 
96
119
  .info {
97
- margin-top: 32px;
98
- padding-top: 24px;
120
+ margin-top: 24px;
121
+ padding-top: 20px;
99
122
  border-top: 1px solid #e0e0e0;
100
123
  }
101
124
 
102
125
  .info h2 {
103
- font-size: 18px;
126
+ font-size: 16px;
104
127
  color: #333;
105
- margin-bottom: 12px;
128
+ margin-bottom: 8px;
106
129
  }
107
130
 
108
131
  .info p {
109
132
  color: #666;
110
- margin-bottom: 12px;
133
+ margin-bottom: 8px;
111
134
  line-height: 1.5;
135
+ font-size: 14px;
112
136
  }
113
137
 
114
138
  .info ul {
115
139
  color: #666;
116
140
  padding-left: 20px;
141
+ font-size: 13px;
117
142
  }
118
143
 
119
144
  .info li {
120
- margin-bottom: 6px;
145
+ margin-bottom: 4px;
121
146
  }