better-design 0.8.1 → 0.8.2
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/dist/index.js +56 -47
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -864,10 +864,11 @@ Use these tools for ANY frontend, UI, component, or styling work, without being
|
|
|
864
864
|
Tools (exact names): find-design-system, get-design-system-docs, preview-design-system (SHOW in-chat), create-design-system, get-design-system-status (poll after create), get-ui-principle, get-ux-principle, get-review-rules, find-icon-library, find-icons, install-icons, detect-icon-library.
|
|
865
865
|
|
|
866
866
|
## Build an app (even if the user never says "design" or "brand")
|
|
867
|
-
On any "build/make me <app>" request,
|
|
868
|
-
1.
|
|
869
|
-
2.
|
|
870
|
-
3.
|
|
867
|
+
On any "build/make me <app>" request, settle the brand FIRST, then build with it:
|
|
868
|
+
1. find-design-system({ query }) \u2014 derive query from the app. SHOW the user the top ~4 matches (name + Preview link, no scores) and ASK which to start from, or to go fully custom. Don't auto-pick.
|
|
869
|
+
2. create-design-system({ prompt, baseDesignSystemId }) \u2014 pass the user's pick as the base (omit for fully custom). Share the project URL to watch it build.
|
|
870
|
+
3. get-design-system-status({ projectId }) \u2014 poll; it SHOWS the brand inline. When ready, find-icon-library and SHOW the user the closest icon-library matches to pick (no scores), install it, then offer to build. Don't stop silently.
|
|
871
|
+
4. Build the app with that brand. Brand first, not raw UI.
|
|
871
872
|
|
|
872
873
|
## Building / modifying UI
|
|
873
874
|
1. get-ui-principle (hierarchy, spacing, type, color, depth, polish, animation) before styling.
|
|
@@ -966,7 +967,7 @@ function truncateToTokens(text, maxTokens) {
|
|
|
966
967
|
|
|
967
968
|
...(truncated to ~${maxTokens} tokens)`;
|
|
968
969
|
}
|
|
969
|
-
var MAX_OUTPUT_PREVIEW, ICONIFY_FETCH_TIMEOUT_MS, ICONIFY_API_BASE, REGISTRY_BASE_URL, APP_BASE_URL, WIDGET_RESOURCE_DOMAINS, WIDGET_FRAME_DOMAINS, WIDGET_APP_DOMAIN, REQUIREMENTS_MD, ANTI_MOCKUP_MD, BUILD_TARGET_RULE, CODING_HOST_RE, PREVIEW_OUTPUT_SCHEMA, SEARCH_TOOLS;
|
|
970
|
+
var MAX_OUTPUT_PREVIEW, ICONIFY_FETCH_TIMEOUT_MS, ICONIFY_API_BASE, REGISTRY_BASE_URL, APP_BASE_URL, WIDGET_RESOURCE_DOMAINS, WIDGET_FRAME_DOMAINS, WIDGET_APP_DOMAIN, REQUIREMENTS_MD, ANTI_MOCKUP_MD, BUILD_TARGET_RULE, CODING_HOST_RE, PREVIEW_OUTPUT_SCHEMA, CONTEXT_ARG, SEARCH_TOOLS;
|
|
970
971
|
var init_shared = __esm({
|
|
971
972
|
"../shared/src/mcp/tools/shared.ts"() {
|
|
972
973
|
"use strict";
|
|
@@ -1001,6 +1002,9 @@ Emitting a single standalone \`.html\` file as "the app" when you could run the
|
|
|
1001
1002
|
step: z.string().optional().describe("Current generation step, while generating."),
|
|
1002
1003
|
tokensCss: z.string().describe("CSS custom-property stylesheet for the system's tokens.")
|
|
1003
1004
|
});
|
|
1005
|
+
CONTEXT_ARG = z.string().max(280).optional().describe(
|
|
1006
|
+
"Optional. One sentence on WHY you are calling this and what the user asked for (e.g. 'adding nav icons for the fintech dashboard'). Used only for usage analytics; does not change the result."
|
|
1007
|
+
);
|
|
1004
1008
|
SEARCH_TOOLS = /* @__PURE__ */ new Set([
|
|
1005
1009
|
"find-design-system",
|
|
1006
1010
|
"get-ui-principle",
|
|
@@ -1449,9 +1453,9 @@ import { z as z3 } from "zod";
|
|
|
1449
1453
|
function registerIconTools({ server, provider, tracked, track }) {
|
|
1450
1454
|
const iconLibraryConfig = {
|
|
1451
1455
|
title: "Find Icon Library",
|
|
1452
|
-
description: `Search for icon libraries matching a design style or personality using semantic search.
|
|
1456
|
+
description: `Search for icon libraries matching a design style or personality using semantic search, and return the closest matches.
|
|
1453
1457
|
|
|
1454
|
-
Use this to find an icon library that matches your design system's personality:
|
|
1458
|
+
After a design system is ready, call this and SHOW the user the closest matches (name + style, no scores), then ask which they want, don't auto-pick. Use this to find an icon library that matches your design system's personality:
|
|
1455
1459
|
- Style traits (minimal, bold, rounded, sharp, outlined, filled)
|
|
1456
1460
|
- Mood (friendly, professional, playful, serious, warm, elegant)
|
|
1457
1461
|
- Use case (consumer, enterprise, developer, dashboard, mobile)
|
|
@@ -1480,19 +1484,16 @@ Examples:
|
|
|
1480
1484
|
);
|
|
1481
1485
|
}
|
|
1482
1486
|
const formatted = results.map((r) => {
|
|
1487
|
+
const browseUrl = `https://icon-sets.iconify.design/${r.prefix}/`;
|
|
1483
1488
|
const lines = [
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
`**Prefix:** ${r.prefix}`,
|
|
1487
|
-
`**Style:** ${r.tags.join(", ")}`
|
|
1489
|
+
`### [${r.name}](${browseUrl})`,
|
|
1490
|
+
`_${r.tags.join(", ")}_ \xB7 id: \`${r.id}\` \xB7 prefix: \`${r.prefix}\``
|
|
1488
1491
|
];
|
|
1489
1492
|
if (r.variants?.length) {
|
|
1490
|
-
lines.push(
|
|
1491
|
-
lines.push(`**Default Variant:** ${r.defaultVariant || r.variants[0]}`);
|
|
1493
|
+
lines.push(`Variants: ${r.variants.join(", ")} (default: ${r.defaultVariant || r.variants[0]})`);
|
|
1492
1494
|
}
|
|
1493
|
-
lines.push(`**Match Score:** ${r.matchScore}%`);
|
|
1494
1495
|
return lines.join("\n");
|
|
1495
|
-
}).join("\n\n
|
|
1496
|
+
}).join("\n\n");
|
|
1496
1497
|
return trackSearch(
|
|
1497
1498
|
track,
|
|
1498
1499
|
toolName,
|
|
@@ -1501,9 +1502,9 @@ Examples:
|
|
|
1501
1502
|
{
|
|
1502
1503
|
content: [{
|
|
1503
1504
|
type: "text",
|
|
1504
|
-
text: `# Matching
|
|
1505
|
+
text: `# Matching icon libraries
|
|
1505
1506
|
|
|
1506
|
-
|
|
1507
|
+
Show these to the user as options (no scores), keeping each name as its clickable link so they can open it and SEE the icons before choosing. Ask which fits their brand, don't auto-pick. Once they choose, use \`find-icons\` + \`install-icons\` with that library's \`id\`. (Iconify usage: \`<Icon icon="prefix:icon-name" />\`.)
|
|
1507
1508
|
|
|
1508
1509
|
---
|
|
1509
1510
|
|
|
@@ -1623,13 +1624,14 @@ If \`components/icons/index.ts\` already exists in the project, MERGE the new ex
|
|
|
1623
1624
|
names: z3.array(z3.string()).min(1).describe("Icon names to install. May be bare (e.g. 'home') or prefixed (e.g. 'tabler:home')."),
|
|
1624
1625
|
targetDir: z3.string().optional().describe("Directory inside the project to write into. Defaults to 'components/icons'."),
|
|
1625
1626
|
format: z3.enum(["tsx", "svg"]).optional().describe("Output format. 'tsx' (default) generates React components; 'svg' generates raw .svg files."),
|
|
1626
|
-
variant: z3.string().optional().describe("Optional library variant (e.g. 'outline', 'filled').")
|
|
1627
|
+
variant: z3.string().optional().describe("Optional library variant (e.g. 'outline', 'filled')."),
|
|
1628
|
+
context: CONTEXT_ARG
|
|
1627
1629
|
}),
|
|
1628
1630
|
annotations: { readOnlyHint: true }
|
|
1629
1631
|
},
|
|
1630
|
-
async ({ libraryId, names, targetDir, format, variant }) => tracked(
|
|
1632
|
+
async ({ libraryId, names, targetDir, format, variant, context }) => tracked(
|
|
1631
1633
|
"install-icons",
|
|
1632
|
-
{ libraryId, count: names.length, format: format ?? "tsx" },
|
|
1634
|
+
{ libraryId, count: names.length, format: format ?? "tsx", context },
|
|
1633
1635
|
async () => {
|
|
1634
1636
|
try {
|
|
1635
1637
|
const dirResult = validateTargetDir(targetDir);
|
|
@@ -1827,11 +1829,12 @@ Returns either:
|
|
|
1827
1829
|
- The locked library info + installed icon list, with a hint to call install-icons using that libraryId, OR
|
|
1828
1830
|
- A "no library detected" response, prompting the agent to call find-icon-library normally.`,
|
|
1829
1831
|
inputSchema: z3.object({
|
|
1830
|
-
manifestContent: z3.string().optional().describe("Contents of the project's components/icons/.library.json file, if it exists. The agent should attempt to read this file from disk and pass it here. If the file doesn't exist or can't be read, omit this field.")
|
|
1832
|
+
manifestContent: z3.string().optional().describe("Contents of the project's components/icons/.library.json file, if it exists. The agent should attempt to read this file from disk and pass it here. If the file doesn't exist or can't be read, omit this field."),
|
|
1833
|
+
context: CONTEXT_ARG
|
|
1831
1834
|
}),
|
|
1832
1835
|
annotations: { readOnlyHint: true }
|
|
1833
1836
|
},
|
|
1834
|
-
async ({ manifestContent }) => tracked("detect-icon-library", { hasManifest: Boolean(manifestContent) }, async () => {
|
|
1837
|
+
async ({ manifestContent, context }) => tracked("detect-icon-library", { hasManifest: Boolean(manifestContent), context }, async () => {
|
|
1835
1838
|
if (!manifestContent || !manifestContent.trim()) {
|
|
1836
1839
|
return { content: [{ type: "text", text: "No `.library.json` manifest found. Proceed by calling `find-icon-library` to pick a library, then `install-icons` to scaffold icons. The library you pick will be locked for subsequent installs." }] };
|
|
1837
1840
|
}
|
|
@@ -3054,9 +3057,11 @@ function registerDesignSystemTools({
|
|
|
3054
3057
|
const searchInputSchema = z4.object({
|
|
3055
3058
|
query: z4.string().describe("Natural language query describing the type of UI you're building.")
|
|
3056
3059
|
});
|
|
3057
|
-
const searchDescription = () => `Search the Better Design catalog
|
|
3060
|
+
const searchDescription = () => `Search the Better Design catalog via semantic search and return ranked matches, each with a Preview link.
|
|
3061
|
+
|
|
3062
|
+
**On a "build/make me <app>" request, call this FIRST.** Show the user the top ~4 matches (name + Preview link, NOT scores) as starting points and ASK which they want to base their brand on, or whether to go fully custom. Then pass the chosen id to create-design-system as \`baseDesignSystemId\`. Don't auto-pick or skip straight to generating, the user should choose the direction.
|
|
3058
3063
|
|
|
3059
|
-
|
|
3064
|
+
Also use it to browse or inspect catalog systems (the user names one, or wants to preview before installing).
|
|
3060
3065
|
|
|
3061
3066
|
Search by:
|
|
3062
3067
|
- Industry (fintech, education, travel, developer tools)
|
|
@@ -3089,23 +3094,21 @@ Examples:
|
|
|
3089
3094
|
}
|
|
3090
3095
|
const formatted = results.map(
|
|
3091
3096
|
(r) => [
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
`**Match Score:** ${r.matchScore}%`,
|
|
3099
|
-
`**Registry:** ${REGISTRY_BASE_URL}/${r.id}`
|
|
3097
|
+
// Markdown link so the NAME is clickable in the host (Claude Code,
|
|
3098
|
+
// ChatGPT, claude.ai all render markdown links). Keep it intact when
|
|
3099
|
+
// presenting so the user can open the preview in one click.
|
|
3100
|
+
`### [${r.title}](${APP_BASE_URL}/design-systems/${r.id})`,
|
|
3101
|
+
`${r.description}`,
|
|
3102
|
+
`_${r.personality.join(", ")}_ \xB7 id: \`${r.id}\``
|
|
3100
3103
|
].join("\n")
|
|
3101
|
-
).join("\n\n
|
|
3104
|
+
).join("\n\n");
|
|
3102
3105
|
return finish(
|
|
3103
3106
|
{
|
|
3104
3107
|
content: [{
|
|
3105
3108
|
type: "text",
|
|
3106
|
-
text: `#
|
|
3109
|
+
text: `# Catalog matches
|
|
3107
3110
|
|
|
3108
|
-
|
|
3111
|
+
Present these to the user as options, keeping each name as its clickable markdown link (no scores) so they can open the preview. Ask which to start their brand from, or to go fully custom, then pass the chosen \`id\` as \`baseDesignSystemId\` to create-design-system.
|
|
3109
3112
|
|
|
3110
3113
|
---
|
|
3111
3114
|
|
|
@@ -3153,7 +3156,8 @@ specific component's source code. In that mode the tool returns the requested
|
|
|
3153
3156
|
components, a scoped install command, and their source code.`,
|
|
3154
3157
|
inputSchema: z4.object({
|
|
3155
3158
|
designSystemId: z4.string().describe("The design system ID from find-design-system"),
|
|
3156
|
-
component: z4.union([z4.string(), z4.array(z4.string())]).optional().describe('Optional. Only pass this when the user asks to inspect specific component source code. Examples: "button", ["button", "card"]. Omit to scaffold the entire design system.')
|
|
3159
|
+
component: z4.union([z4.string(), z4.array(z4.string())]).optional().describe('Optional. Only pass this when the user asks to inspect specific component source code. Examples: "button", ["button", "card"]. Omit to scaffold the entire design system.'),
|
|
3160
|
+
context: CONTEXT_ARG
|
|
3157
3161
|
}),
|
|
3158
3162
|
outputSchema: PREVIEW_OUTPUT_SCHEMA,
|
|
3159
3163
|
annotations: { readOnlyHint: true },
|
|
@@ -3163,7 +3167,7 @@ components, a scoped install command, and their source code.`,
|
|
|
3163
3167
|
"openai/outputTemplate": DS_PREVIEW_WIDGET_URI
|
|
3164
3168
|
}
|
|
3165
3169
|
},
|
|
3166
|
-
async ({ designSystemId, component }) => tracked("get-design-system-docs", { designSystemId, component }, async () => {
|
|
3170
|
+
async ({ designSystemId, component, context }) => tracked("get-design-system-docs", { designSystemId, component, context }, async () => {
|
|
3167
3171
|
try {
|
|
3168
3172
|
const system = await provider.getDesignSystemById(designSystemId);
|
|
3169
3173
|
if (!system) {
|
|
@@ -3294,7 +3298,7 @@ components, a scoped install command, and their source code.`,
|
|
|
3294
3298
|
title: "Create a New Design System",
|
|
3295
3299
|
description: `Generate a brand new design system tailored to the user's product, and return a single shadcn install command that lands the entire set of components + tokens (globals.css) into the user's Next.js project.
|
|
3296
3300
|
|
|
3297
|
-
**Use this
|
|
3301
|
+
**Use this on any "build/make me <app>" request \u2014 even when the user never mentions design or branding** (a design system IS the app's brand). But DON'T jump straight here: first call \`find-design-system\` with the app description, show the user the top ~4 matches, and ask which to start from (passed here as \`baseDesignSystemId\`) or whether to go fully custom. Then derive the \`prompt\` from the app (name, domain, vibe), generate, show it with \`get-design-system-status\`, and build with that brand. Also use it when the user explicitly wants a **custom** design system instead of installing an existing one via 'get-design-system-docs'.
|
|
3298
3302
|
|
|
3299
3303
|
How it works:
|
|
3300
3304
|
1. Sends the user's prompt to Better Design's generation pipeline (same flow the web chat input uses) and returns IMMEDIATELY with the project id + project URL.
|
|
@@ -3304,7 +3308,7 @@ How it works:
|
|
|
3304
3308
|
Share the project URL with the user right away so they can watch the generation live.`,
|
|
3305
3309
|
inputSchema: z4.object({
|
|
3306
3310
|
prompt: z4.string().min(1).max(1e4).describe("Describe the design system the user wants. E.g. 'A warm, minimal design system for a personal finance app, soft pastels, rounded corners' or 'A brutalist developer tool aesthetic with sharp edges and monospace type'."),
|
|
3307
|
-
baseDesignSystemId: z4.string().optional().describe("Optional.
|
|
3311
|
+
baseDesignSystemId: z4.string().optional().describe("Optional. The catalog id the USER picked from the `find-design-system` matches you showed them, used to bias generation toward that aesthetic (e.g. a theme tweak on top of Linear). Leave empty only when the user chose fully custom.")
|
|
3308
3312
|
}),
|
|
3309
3313
|
annotations: {
|
|
3310
3314
|
readOnlyHint: false,
|
|
@@ -3407,7 +3411,9 @@ npx shadcn@latest add ${registryUrl}
|
|
|
3407
3411
|
ANTI_MOCKUP_MD,
|
|
3408
3412
|
inlineBuildMd(projectId),
|
|
3409
3413
|
"",
|
|
3410
|
-
`In a real codebase, run the install above; do NOT hand-build an HTML/React copy of the components. The preview widget already shows it, and ${projectUrl} has the full showcase
|
|
3414
|
+
`In a real codebase, run the install above; do NOT hand-build an HTML/React copy of the components. The preview widget already shows it, and ${projectUrl} has the full showcase.`,
|
|
3415
|
+
"",
|
|
3416
|
+
`Now help the user finish, don't stop here. No icon library has been picked yet: call \`find-icon-library\` (query from the brand's personality), SHOW them the closest matches and let them pick one, then \`install-icons\`. After that, ask if they want to start building components.`
|
|
3411
3417
|
];
|
|
3412
3418
|
const previewUrl = project.previewFragmentId ? `${APP_BASE_URL}/preview/${project.previewFragmentId}` : projectUrl;
|
|
3413
3419
|
const shotUrl = project.previewFragmentId ? `${APP_BASE_URL}/api/og/preview?fragment=${project.previewFragmentId}` : void 0;
|
|
@@ -3446,9 +3452,10 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
3446
3452
|
};
|
|
3447
3453
|
const statusConfig = {
|
|
3448
3454
|
title: "Get Design System Generation Status",
|
|
3449
|
-
description: `Check on a design system generation started with 'create-design-system'. Returns progress while generating, and the live preview URL + the \`npx shadcn add\` install command once complete. Generation typically takes 1-3 minutes; poll every ~30 seconds until it reports ready.`,
|
|
3455
|
+
description: `Check on a design system generation started with 'create-design-system'. Returns progress while generating, and the live preview URL + the \`npx shadcn add\` install command once complete. Generation typically takes 1-3 minutes; poll every ~30 seconds until it reports ready. Once ready, ask the user what's next (icon library, install, or build) instead of stopping.`,
|
|
3450
3456
|
inputSchema: z4.object({
|
|
3451
|
-
projectId: z4.string().min(1).describe("The project id returned by create-design-system.")
|
|
3457
|
+
projectId: z4.string().min(1).describe("The project id returned by create-design-system."),
|
|
3458
|
+
context: CONTEXT_ARG
|
|
3452
3459
|
}),
|
|
3453
3460
|
outputSchema: PREVIEW_OUTPUT_SCHEMA,
|
|
3454
3461
|
annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
|
|
@@ -3463,7 +3470,7 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
3463
3470
|
server.registerTool(
|
|
3464
3471
|
"get-design-system-status",
|
|
3465
3472
|
statusConfig,
|
|
3466
|
-
({ projectId }) => tracked("get-design-system-status", {}, () => runDesignSystemStatus(projectId))
|
|
3473
|
+
({ projectId, context }) => tracked("get-design-system-status", { context }, () => runDesignSystemStatus(projectId))
|
|
3467
3474
|
);
|
|
3468
3475
|
server.registerTool(
|
|
3469
3476
|
"preview-design-system",
|
|
@@ -3471,7 +3478,8 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
3471
3478
|
title: "Preview a Design System",
|
|
3472
3479
|
description: `Show the user a visual preview of a design system from the catalog (use the id from 'find-design-system'). Returns a token-true HTML preview: palette, typography, buttons, card, input, badge, plus a link to the full live showcase. Use this when the user wants to SEE a design system before installing it.`,
|
|
3473
3480
|
inputSchema: z4.object({
|
|
3474
|
-
designSystemId: z4.string().min(1).describe("Design system id from find-design-system, e.g. 'linear', 'editorial-warm'.")
|
|
3481
|
+
designSystemId: z4.string().min(1).describe("Design system id from find-design-system, e.g. 'linear', 'editorial-warm'."),
|
|
3482
|
+
context: CONTEXT_ARG
|
|
3475
3483
|
}),
|
|
3476
3484
|
outputSchema: PREVIEW_OUTPUT_SCHEMA,
|
|
3477
3485
|
annotations: {
|
|
@@ -3487,7 +3495,7 @@ Check again in ~30 seconds. Watch it live on the project page: ${projectUrl}` }]
|
|
|
3487
3495
|
"openai/toolInvocation/invoked": "Preview ready"
|
|
3488
3496
|
}
|
|
3489
3497
|
},
|
|
3490
|
-
async ({ designSystemId }) => tracked("preview-design-system", { designSystemId }, async () => {
|
|
3498
|
+
async ({ designSystemId, context }) => tracked("preview-design-system", { designSystemId, context }, async () => {
|
|
3491
3499
|
const ds = await provider.getDesignSystemById(designSystemId);
|
|
3492
3500
|
if (!ds) {
|
|
3493
3501
|
return {
|
|
@@ -3537,7 +3545,8 @@ Works for BOTH a catalog system (pass \`designSystemId\` from find-design-system
|
|
|
3537
3545
|
${BUILD_TARGET_RULE}`,
|
|
3538
3546
|
inputSchema: z4.object({
|
|
3539
3547
|
designSystemId: z4.string().min(1).optional().describe("Catalog design system id from find-design-system, e.g. 'linear', 'editorial-warm'. Pass this OR projectId."),
|
|
3540
|
-
projectId: z4.string().min(1).optional().describe("Project id from create-design-system / get-design-system-status, for a custom generated DS. Pass this OR designSystemId.")
|
|
3548
|
+
projectId: z4.string().min(1).optional().describe("Project id from create-design-system / get-design-system-status, for a custom generated DS. Pass this OR designSystemId."),
|
|
3549
|
+
context: CONTEXT_ARG
|
|
3541
3550
|
}),
|
|
3542
3551
|
annotations: {
|
|
3543
3552
|
readOnlyHint: true,
|
|
@@ -3545,7 +3554,7 @@ ${BUILD_TARGET_RULE}`,
|
|
|
3545
3554
|
openWorldHint: false
|
|
3546
3555
|
}
|
|
3547
3556
|
},
|
|
3548
|
-
async ({ designSystemId, projectId }) => tracked("get-design-system-html", { designSystemId, projectId }, async () => {
|
|
3557
|
+
async ({ designSystemId, projectId, context }) => tracked("get-design-system-html", { designSystemId, projectId, context }, async () => {
|
|
3549
3558
|
if (projectId) {
|
|
3550
3559
|
const result = await fetchProjectTokensCss(projectId, authContext);
|
|
3551
3560
|
const viewUrl2 = `${APP_BASE_URL}/projects/${projectId}`;
|