@techstream/quark-create-app 1.9.0 → 1.10.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.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/index.js +376 -143
- package/templates/base-project/.cursor/rules/quark.mdc +172 -0
- package/templates/base-project/.github/copilot-instructions.md +55 -0
- package/templates/base-project/CLAUDE.md +273 -0
- package/templates/base-project/README.md +72 -30
- package/templates/base-project/apps/web/next.config.js +5 -1
- package/templates/base-project/apps/web/package.json +3 -3
- package/templates/base-project/apps/web/public/quark.svg +46 -0
- package/templates/base-project/apps/web/railway.json +2 -2
- package/templates/base-project/apps/web/src/app/_components/HealthIndicator.js +85 -0
- package/templates/base-project/apps/web/src/app/_components/HomeThemeToggle.js +63 -0
- package/templates/base-project/apps/web/src/app/_components/QuarkAnimation.js +168 -0
- package/templates/base-project/apps/web/src/app/api/health/route.js +28 -17
- package/templates/base-project/apps/web/src/app/favicon.ico +0 -0
- package/templates/base-project/apps/web/src/app/global-error.js +53 -0
- package/templates/base-project/apps/web/src/app/globals.css +121 -15
- package/templates/base-project/apps/web/src/app/icon.svg +46 -0
- package/templates/base-project/apps/web/src/app/layout.js +1 -0
- package/templates/base-project/apps/web/src/app/not-found.js +35 -0
- package/templates/base-project/apps/web/src/app/page.js +38 -5
- package/templates/base-project/apps/web/src/lib/theme.js +23 -0
- package/templates/base-project/apps/web/src/proxy.js +10 -2
- package/templates/base-project/package.json +2 -0
- package/templates/base-project/packages/db/src/client.js +6 -1
- package/templates/base-project/packages/db/src/index.js +1 -0
- package/templates/base-project/packages/db/src/ping.js +66 -0
- package/templates/base-project/scripts/doctor.js +261 -0
- package/templates/base-project/turbo.json +2 -1
- package/templates/config/package.json +1 -0
- package/templates/jobs/package.json +2 -1
- package/templates/ui/README.md +67 -0
- package/templates/ui/package.json +1 -0
- package/templates/ui/src/badge.js +32 -0
- package/templates/ui/src/badge.test.js +42 -0
- package/templates/ui/src/button.js +64 -15
- package/templates/ui/src/button.test.js +34 -5
- package/templates/ui/src/card.js +58 -0
- package/templates/ui/src/card.test.js +59 -0
- package/templates/ui/src/checkbox.js +35 -0
- package/templates/ui/src/checkbox.test.js +35 -0
- package/templates/ui/src/dialog.js +139 -0
- package/templates/ui/src/dialog.test.js +15 -0
- package/templates/ui/src/index.js +16 -0
- package/templates/ui/src/input.js +15 -0
- package/templates/ui/src/input.test.js +27 -0
- package/templates/ui/src/label.js +14 -0
- package/templates/ui/src/label.test.js +22 -0
- package/templates/ui/src/select.js +42 -0
- package/templates/ui/src/select.test.js +27 -0
- package/templates/ui/src/skeleton.js +14 -0
- package/templates/ui/src/skeleton.test.js +22 -0
- package/templates/ui/src/table.js +75 -0
- package/templates/ui/src/table.test.js +69 -0
- package/templates/ui/src/textarea.js +15 -0
- package/templates/ui/src/textarea.test.js +27 -0
- package/templates/ui/src/theme-constants.js +24 -0
- package/templates/ui/src/theme.js +132 -0
- package/templates/ui/src/toast.js +229 -0
- package/templates/ui/src/toast.test.js +23 -0
- package/templates/{base-project/apps/worker → worker}/package.json +2 -2
- package/templates/{base-project/apps/worker → worker}/src/index.js +38 -23
- package/templates/{base-project/apps/worker → worker}/src/index.test.js +19 -20
- package/templates/base-project/apps/web/public/file.svg +0 -1
- package/templates/base-project/apps/web/public/globe.svg +0 -1
- package/templates/base-project/apps/web/public/next.svg +0 -1
- package/templates/base-project/apps/web/public/vercel.svg +0 -1
- package/templates/base-project/apps/web/public/window.svg +0 -1
- /package/templates/{base-project/apps/worker → worker}/README.md +0 -0
- /package/templates/{base-project/apps/worker → worker}/railway.json +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/email.js +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/files.js +0 -0
- /package/templates/{base-project/apps/worker → worker}/src/handlers/index.js +0 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import assert from "node:assert";
|
|
2
|
+
import { test } from "node:test";
|
|
3
|
+
import { Toast, useToast } from "./toast.js";
|
|
4
|
+
|
|
5
|
+
// Toast uses React hooks (useState, useEffect, useRef) and must be rendered
|
|
6
|
+
// via a React renderer. Direct invocation in node --test is not supported.
|
|
7
|
+
// Tests here verify the module exports correct types only.
|
|
8
|
+
|
|
9
|
+
test("Toast - exports correctly", () => {
|
|
10
|
+
assert.strictEqual(typeof Toast, "function");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
test("Toast - has expected function name", () => {
|
|
14
|
+
assert.strictEqual(Toast.name, "Toast");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
test("useToast - exports correctly", () => {
|
|
18
|
+
assert.strictEqual(typeof useToast, "function");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("useToast - has expected function name", () => {
|
|
22
|
+
assert.strictEqual(useToast.name, "useToast");
|
|
23
|
+
});
|
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
"@techstream/quark-core": "^1.0.0",
|
|
21
21
|
"@techstream/quark-db": "workspace:*",
|
|
22
22
|
"@techstream/quark-jobs": "workspace:*",
|
|
23
|
-
"bullmq": "^5.70.
|
|
23
|
+
"bullmq": "^5.70.4"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|
|
26
|
-
"@types/node": "^25.3.
|
|
26
|
+
"@types/node": "^25.3.5",
|
|
27
27
|
"tsx": "^4.21.0"
|
|
28
28
|
}
|
|
29
29
|
}
|
|
@@ -9,15 +9,20 @@ import {
|
|
|
9
9
|
createLogger,
|
|
10
10
|
createQueue,
|
|
11
11
|
createWorker,
|
|
12
|
+
getRedisUrl,
|
|
12
13
|
} from "@techstream/quark-core";
|
|
13
14
|
import { prisma } from "@techstream/quark-db";
|
|
14
15
|
import { JOB_NAMES, JOB_QUEUES } from "@techstream/quark-jobs";
|
|
15
16
|
import { jobHandlers } from "./handlers/index.js";
|
|
16
17
|
|
|
17
18
|
const logger = createLogger("worker");
|
|
19
|
+
const isDevMode =
|
|
20
|
+
process.env.NODE_ENV !== "production" &&
|
|
21
|
+
process.env.npm_lifecycle_event === "dev";
|
|
18
22
|
|
|
19
23
|
// Store workers for graceful shutdown
|
|
20
24
|
const workers = [];
|
|
25
|
+
let devDisabledKeepAlive = null;
|
|
21
26
|
let isShuttingDown = false;
|
|
22
27
|
|
|
23
28
|
// ============================================================================
|
|
@@ -40,6 +45,7 @@ export function isConnectionError(error) {
|
|
|
40
45
|
"EHOSTUNREACH", // Host unreachable
|
|
41
46
|
"ENETUNREACH", // Network unreachable
|
|
42
47
|
"Error: Redis connection failed", // Generic redis failure
|
|
48
|
+
"Redis unavailable at", // Final wrapped startup error
|
|
43
49
|
"Ready status is false", // BullMQ readiness
|
|
44
50
|
];
|
|
45
51
|
return connectionErrors.some((err) => message.includes(err));
|
|
@@ -62,10 +68,8 @@ export function throttledError(logger, windowMs = 5000) {
|
|
|
62
68
|
|
|
63
69
|
// Log if new error type or window expired
|
|
64
70
|
if (msg !== lastErrorMsg || now - lastErrorTime > windowMs) {
|
|
65
|
-
logger.
|
|
66
|
-
|
|
67
|
-
code: error.code,
|
|
68
|
-
timestamp: new Date().toISOString(),
|
|
71
|
+
logger.warn("Waiting for Redis", {
|
|
72
|
+
reason: msg,
|
|
69
73
|
});
|
|
70
74
|
lastErrorTime = now;
|
|
71
75
|
lastErrorMsg = msg;
|
|
@@ -73,6 +77,14 @@ export function throttledError(logger, windowMs = 5000) {
|
|
|
73
77
|
};
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
function disableWorkerInDev() {
|
|
81
|
+
if (!devDisabledKeepAlive) {
|
|
82
|
+
// Keep the process alive so the dev session stays healthy even when the
|
|
83
|
+
// worker is intentionally disabled due to missing Redis.
|
|
84
|
+
devDisabledKeepAlive = setInterval(() => {}, 60_000);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
76
88
|
/**
|
|
77
89
|
* Waits for Redis to be ready with retries
|
|
78
90
|
* @param {Function} healthCheck - Async function that returns boolean or throws
|
|
@@ -91,7 +103,6 @@ export async function waitForRedis(
|
|
|
91
103
|
} = config;
|
|
92
104
|
|
|
93
105
|
const reportThrottledError = throttledError(logger, 3000);
|
|
94
|
-
let lastError;
|
|
95
106
|
|
|
96
107
|
for (let attempt = 1; attempt <= maxRetries; attempt++) {
|
|
97
108
|
try {
|
|
@@ -103,30 +114,20 @@ export async function waitForRedis(
|
|
|
103
114
|
return true;
|
|
104
115
|
}
|
|
105
116
|
} catch (error) {
|
|
106
|
-
lastError = error;
|
|
107
117
|
if (isConnectionError(error)) {
|
|
108
118
|
reportThrottledError(error);
|
|
109
119
|
if (attempt < maxRetries) {
|
|
110
|
-
// Wait before retrying
|
|
111
120
|
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
112
121
|
}
|
|
113
122
|
} else {
|
|
114
|
-
|
|
115
|
-
logger.error("Health check failed with non-network error", {
|
|
116
|
-
error: error.message,
|
|
117
|
-
});
|
|
118
|
-
throw error;
|
|
123
|
+
throw new Error(`Redis health check failed: ${error.message}`);
|
|
119
124
|
}
|
|
120
125
|
}
|
|
121
126
|
}
|
|
122
127
|
|
|
123
128
|
// All retries exhausted
|
|
124
|
-
logger.error("Redis health check failed after all retries", {
|
|
125
|
-
attempts: maxRetries,
|
|
126
|
-
lastError: lastError?.message,
|
|
127
|
-
});
|
|
128
129
|
throw new Error(
|
|
129
|
-
`
|
|
130
|
+
`Redis unavailable at ${getRedisUrl()} after ${maxRetries} attempts. Start Redis or check REDIS_URL/REDIS_HOST/REDIS_PORT.`,
|
|
130
131
|
);
|
|
131
132
|
}
|
|
132
133
|
|
|
@@ -148,10 +149,7 @@ async function preflight() {
|
|
|
148
149
|
try {
|
|
149
150
|
// Check Redis
|
|
150
151
|
logger.info("Checking Redis connectivity...");
|
|
151
|
-
|
|
152
|
-
if (!redisReady) {
|
|
153
|
-
throw new Error("Redis health check returned false");
|
|
154
|
-
}
|
|
152
|
+
await checkQueueHealth();
|
|
155
153
|
logger.info("✓ Redis connected");
|
|
156
154
|
|
|
157
155
|
// Check Database
|
|
@@ -256,8 +254,12 @@ async function startWorker() {
|
|
|
256
254
|
try {
|
|
257
255
|
// Pre-flight: Wait for Redis with health checks and retries
|
|
258
256
|
logger.info("Performing health checks...");
|
|
259
|
-
await waitForRedis(
|
|
257
|
+
await waitForRedis(
|
|
258
|
+
checkQueueHealth,
|
|
259
|
+
isDevMode ? { maxRetries: 3, intervalMs: 500 } : {},
|
|
260
|
+
);
|
|
260
261
|
|
|
262
|
+
logger.info("Redis connected", { address: getRedisUrl() });
|
|
261
263
|
// Register a worker for each queue
|
|
262
264
|
for (const queueName of Object.values(JOB_QUEUES)) {
|
|
263
265
|
createQueueWorker(queueName);
|
|
@@ -276,9 +278,17 @@ async function startWorker() {
|
|
|
276
278
|
|
|
277
279
|
logger.info("Worker service ready");
|
|
278
280
|
} catch (error) {
|
|
281
|
+
if (isDevMode && isConnectionError(error)) {
|
|
282
|
+
logger.warn("Redis unavailable — worker disabled in dev", {
|
|
283
|
+
action:
|
|
284
|
+
"Start Redis and restart the worker when background jobs are needed.",
|
|
285
|
+
});
|
|
286
|
+
disableWorkerInDev();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
279
290
|
logger.error("Failed to start worker service", {
|
|
280
291
|
error: error.message,
|
|
281
|
-
stack: error.stack,
|
|
282
292
|
});
|
|
283
293
|
process.exit(1);
|
|
284
294
|
}
|
|
@@ -297,6 +307,11 @@ async function shutdown(signal = "unknown") {
|
|
|
297
307
|
logger.info("Shutting down worker service", { signal });
|
|
298
308
|
|
|
299
309
|
try {
|
|
310
|
+
if (devDisabledKeepAlive) {
|
|
311
|
+
clearInterval(devDisabledKeepAlive);
|
|
312
|
+
devDisabledKeepAlive = null;
|
|
313
|
+
}
|
|
314
|
+
|
|
300
315
|
for (const worker of workers) {
|
|
301
316
|
await worker.close();
|
|
302
317
|
}
|
|
@@ -362,9 +362,9 @@ describe("throttledError", () => {
|
|
|
362
362
|
|
|
363
363
|
throttle(new Error("Redis unavailable"));
|
|
364
364
|
|
|
365
|
-
assert.strictEqual(logger.
|
|
366
|
-
const call = logger.
|
|
367
|
-
assert.ok(call.arguments[0].includes("
|
|
365
|
+
assert.strictEqual(logger.warn.mock.callCount(), 1);
|
|
366
|
+
const call = logger.warn.mock.calls[0];
|
|
367
|
+
assert.ok(call.arguments[0].includes("Waiting for Redis"));
|
|
368
368
|
});
|
|
369
369
|
|
|
370
370
|
test("suppresses duplicate errors within window", () => {
|
|
@@ -372,15 +372,15 @@ describe("throttledError", () => {
|
|
|
372
372
|
const throttle = throttledError(logger, 100);
|
|
373
373
|
|
|
374
374
|
throttle(new Error("Redis unavailable"));
|
|
375
|
-
assert.strictEqual(logger.
|
|
375
|
+
assert.strictEqual(logger.warn.mock.callCount(), 1);
|
|
376
376
|
|
|
377
377
|
// Same error within window — should be suppressed
|
|
378
378
|
throttle(new Error("Redis unavailable"));
|
|
379
|
-
assert.strictEqual(logger.
|
|
379
|
+
assert.strictEqual(logger.warn.mock.callCount(), 1);
|
|
380
380
|
|
|
381
381
|
// Different error within window — should log
|
|
382
382
|
throttle(new Error("Redis timeout"));
|
|
383
|
-
assert.strictEqual(logger.
|
|
383
|
+
assert.strictEqual(logger.warn.mock.callCount(), 2);
|
|
384
384
|
});
|
|
385
385
|
|
|
386
386
|
test("logs error again after window expires", async () => {
|
|
@@ -388,18 +388,18 @@ describe("throttledError", () => {
|
|
|
388
388
|
const throttle = throttledError(logger, 50); // 50ms window
|
|
389
389
|
|
|
390
390
|
throttle(new Error("Redis unavailable"));
|
|
391
|
-
assert.strictEqual(logger.
|
|
391
|
+
assert.strictEqual(logger.warn.mock.callCount(), 1);
|
|
392
392
|
|
|
393
393
|
// Same error within window — suppressed
|
|
394
394
|
throttle(new Error("Redis unavailable"));
|
|
395
|
-
assert.strictEqual(logger.
|
|
395
|
+
assert.strictEqual(logger.warn.mock.callCount(), 1);
|
|
396
396
|
|
|
397
397
|
// Wait for window to expire
|
|
398
398
|
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
399
399
|
|
|
400
400
|
// Same error after window — logged again
|
|
401
401
|
throttle(new Error("Redis unavailable"));
|
|
402
|
-
assert.strictEqual(logger.
|
|
402
|
+
assert.strictEqual(logger.warn.mock.callCount(), 2);
|
|
403
403
|
});
|
|
404
404
|
|
|
405
405
|
test("includes error details in log", () => {
|
|
@@ -410,11 +410,10 @@ describe("throttledError", () => {
|
|
|
410
410
|
error.code = "ECONNREFUSED";
|
|
411
411
|
throttle(error);
|
|
412
412
|
|
|
413
|
-
const call = logger.
|
|
413
|
+
const call = logger.warn.mock.calls[0];
|
|
414
414
|
const args = call.arguments;
|
|
415
|
-
assert.strictEqual(args[0], "
|
|
416
|
-
assert.ok(args[1].
|
|
417
|
-
assert.ok(args[1].timestamp); // Should have timestamp
|
|
415
|
+
assert.strictEqual(args[0], "Waiting for Redis");
|
|
416
|
+
assert.ok(args[1].reason.includes("ECONNREFUSED"));
|
|
418
417
|
});
|
|
419
418
|
|
|
420
419
|
test("uses default 5 second window if not specified", async () => {
|
|
@@ -422,14 +421,14 @@ describe("throttledError", () => {
|
|
|
422
421
|
const throttle = throttledError(logger); // No window specified
|
|
423
422
|
|
|
424
423
|
throttle(new Error("Test"));
|
|
425
|
-
assert.strictEqual(logger.
|
|
424
|
+
assert.strictEqual(logger.warn.mock.callCount(), 1);
|
|
426
425
|
|
|
427
426
|
throttle(new Error("Test"));
|
|
428
|
-
assert.strictEqual(logger.
|
|
427
|
+
assert.strictEqual(logger.warn.mock.callCount(), 1); // Suppressed
|
|
429
428
|
|
|
430
429
|
// 5 second default window hasn't expired
|
|
431
430
|
throttle(new Error("Test"));
|
|
432
|
-
assert.strictEqual(logger.
|
|
431
|
+
assert.strictEqual(logger.warn.mock.callCount(), 1); // Still suppressed
|
|
433
432
|
});
|
|
434
433
|
});
|
|
435
434
|
|
|
@@ -479,7 +478,7 @@ describe("waitForRedis", () => {
|
|
|
479
478
|
maxRetries: 2,
|
|
480
479
|
intervalMs: 10,
|
|
481
480
|
}),
|
|
482
|
-
{ message: /
|
|
481
|
+
{ message: /Redis unavailable at .+ after 2 attempts/ },
|
|
483
482
|
);
|
|
484
483
|
|
|
485
484
|
assert.strictEqual(healthCheck.mock.callCount(), 2);
|
|
@@ -496,7 +495,7 @@ describe("waitForRedis", () => {
|
|
|
496
495
|
maxRetries: 5,
|
|
497
496
|
intervalMs: 10,
|
|
498
497
|
}),
|
|
499
|
-
{ message: "Invalid configuration" },
|
|
498
|
+
{ message: "Redis health check failed: Invalid configuration" },
|
|
500
499
|
);
|
|
501
500
|
|
|
502
501
|
// Should fail immediately, not retry 5 times
|
|
@@ -514,7 +513,7 @@ describe("waitForRedis", () => {
|
|
|
514
513
|
maxRetries: 4,
|
|
515
514
|
intervalMs: 10,
|
|
516
515
|
}),
|
|
517
|
-
/
|
|
516
|
+
/Redis unavailable at .+ after 4 attempts/,
|
|
518
517
|
);
|
|
519
518
|
|
|
520
519
|
assert.strictEqual(healthCheck.mock.callCount(), 4);
|
|
@@ -565,7 +564,7 @@ describe("waitForRedis", () => {
|
|
|
565
564
|
// Call without explicit config — should use env defaults
|
|
566
565
|
await assert.rejects(
|
|
567
566
|
() => waitForRedis(healthCheck),
|
|
568
|
-
/
|
|
567
|
+
/Redis unavailable at .+ after 2 attempts/,
|
|
569
568
|
);
|
|
570
569
|
|
|
571
570
|
assert.strictEqual(healthCheck.mock.callCount(), 2);
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|