create-mcp-use-app 2.0.0-beta.0 → 2.0.0-beta.10

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 (48) hide show
  1. package/README.md +74 -47
  2. package/dist/index.js +70 -96
  3. package/dist/templates/blank/README.md +3 -1
  4. package/dist/templates/blank/mcp-env.d.ts +10 -0
  5. package/dist/templates/blank/package.json +16 -8
  6. package/dist/templates/blank/tsconfig.json +14 -14
  7. package/dist/templates/mcp-apps/README.md +3 -1
  8. package/dist/templates/mcp-apps/index.ts +98 -24
  9. package/dist/templates/mcp-apps/mcp-env.d.ts +10 -0
  10. package/dist/templates/mcp-apps/package.json +17 -15
  11. package/dist/templates/mcp-apps/public/logo-mcp-use.svg +8 -0
  12. package/dist/templates/mcp-apps/tsconfig.json +9 -3
  13. package/dist/templates/mcp-apps/views/my-view/MeshGradient.tsx +299 -0
  14. package/dist/templates/mcp-apps/views/my-view/Tooltip.tsx +75 -0
  15. package/dist/templates/mcp-apps/views/my-view/icons.tsx +142 -0
  16. package/dist/templates/mcp-apps/views/my-view/view.css +5 -0
  17. package/dist/templates/mcp-apps/views/my-view/view.tsx +396 -0
  18. package/dist/templates/{starter → mcp-server}/README.md +4 -2
  19. package/dist/templates/{starter → mcp-server}/index.ts +1 -19
  20. package/dist/templates/mcp-server/mcp-env.d.ts +10 -0
  21. package/dist/templates/mcp-server/package.json +37 -0
  22. package/dist/templates/mcp-server/tsconfig.json +24 -0
  23. package/package.json +3 -3
  24. package/dist/templates/blank/public/favicon.ico +0 -0
  25. package/dist/templates/mcp-apps/public/favicon.ico +0 -0
  26. package/dist/templates/mcp-apps/public/fruits/apple.png +0 -0
  27. package/dist/templates/mcp-apps/public/fruits/apricot.png +0 -0
  28. package/dist/templates/mcp-apps/public/fruits/avocado.png +0 -0
  29. package/dist/templates/mcp-apps/public/fruits/banana.png +0 -0
  30. package/dist/templates/mcp-apps/public/fruits/blueberry.png +0 -0
  31. package/dist/templates/mcp-apps/public/fruits/cherries.png +0 -0
  32. package/dist/templates/mcp-apps/public/fruits/coconut.png +0 -0
  33. package/dist/templates/mcp-apps/public/fruits/grapes.png +0 -0
  34. package/dist/templates/mcp-apps/public/fruits/lemon.png +0 -0
  35. package/dist/templates/mcp-apps/public/fruits/mango.png +0 -0
  36. package/dist/templates/mcp-apps/public/fruits/orange.png +0 -0
  37. package/dist/templates/mcp-apps/public/fruits/pear.png +0 -0
  38. package/dist/templates/mcp-apps/public/fruits/pineapple.png +0 -0
  39. package/dist/templates/mcp-apps/public/fruits/plum.png +0 -0
  40. package/dist/templates/mcp-apps/public/fruits/strawberry.png +0 -0
  41. package/dist/templates/mcp-apps/public/fruits/watermelon.png +0 -0
  42. package/dist/templates/mcp-apps/resources/product-search-result/view.tsx +0 -26
  43. package/dist/templates/mcp-apps/src/register.d.ts +0 -7
  44. package/dist/templates/starter/package.json +0 -32
  45. package/dist/templates/starter/public/favicon.ico +0 -0
  46. package/dist/templates/starter/tsconfig.json +0 -24
  47. /package/dist/templates/{starter → mcp-server}/gitignore +0 -0
  48. /package/dist/templates/{starter → mcp-server}/public/icon.svg +0 -0
@@ -1,24 +1,24 @@
1
1
  {
2
2
  "compilerOptions": {
3
- "target": "ES2022",
3
+ "target": "ES2024",
4
4
  "jsx": "react-jsx",
5
- "module": "ESNext",
6
- "moduleResolution": "bundler",
7
- "allowJs": true,
5
+ "module": "NodeNext",
6
+ "moduleResolution": "NodeNext",
7
+ "lib": ["ES2024", "DOM", "DOM.Iterable"],
8
8
  "strict": true,
9
- "outDir": "./dist",
10
- "sourceMap": true,
11
- "allowSyntheticDefaultImports": true,
12
9
  "esModuleInterop": true,
13
10
  "forceConsistentCasingInFileNames": true,
14
11
  "skipLibCheck": true,
15
- "incremental": true,
16
- "tsBuildInfoFile": "./dist/.tsbuildinfo",
17
- "baseUrl": ".",
18
- "paths": {
19
- "@/*": ["src/*"]
20
- }
12
+ "noEmit": true,
13
+ "noUncheckedSideEffectImports": false
21
14
  },
22
- "include": ["index.ts", "src/**/*", "resources/**/*", "server.ts", ".mcp-use/**/*.d.ts"],
15
+ "include": [
16
+ "index.ts",
17
+ "mcp-env.d.ts",
18
+ "src/**/*",
19
+ "views/**/*",
20
+ "server.ts",
21
+ ".mcp-use/**/*.d.ts"
22
+ ],
23
23
  "exclude": ["node_modules", "dist"]
24
24
  }
@@ -11,10 +11,12 @@ npm install
11
11
  npm run dev
12
12
  ```
13
13
 
14
- Open [http://localhost:3000/inspector](http://localhost:3000/inspector) with your browser to test your server.
14
+ Open [http://localhost:3000/mcp/inspector](http://localhost:3000/mcp/inspector) with your browser to test your server.
15
15
 
16
16
  You can start building by editing the entry file. Add tools, resources, and prompts — the server auto-reloads as you edit.
17
17
 
18
+ Run `npm run typecheck` to refresh MCP view types and check the project with its local TypeScript compiler. Statically declared tools used by views must be assigned to exported constants.
19
+
18
20
  ## Learn More
19
21
 
20
22
  To learn more about mcp-use and MCP:
@@ -1,40 +1,114 @@
1
1
  import { MCPServer } from "mcp-use";
2
2
  import { z } from "zod";
3
3
 
4
- const fruits = ["apple", "banana", "mango", "orange", "strawberry"];
5
-
6
- const outputSchema = z.object({
7
- query: z.string(),
8
- items: z.array(z.object({ id: z.string(), name: z.string() })),
9
- });
10
-
11
4
  const server = new MCPServer({
12
5
  name: "{{PROJECT_NAME}}",
13
- title: "{{PROJECT_NAME}}",
6
+ title: "{{PROJECT_NAME}}", // Human readable name of the server
14
7
  version: "1.0.0",
15
- description: "An MCP Apps server built with mcp-use",
8
+ description: "an mcp-use app",
9
+ instructions: "use show-app to open the app view", // Model-facing guidance — surfaced to the LLM by compatible clients.
10
+ websiteUrl: "https://mcp-use.com",
11
+ // Icons for your MCP Server, from public/ (or absolute URLs).
12
+ icons: [
13
+ {
14
+ src: "icon.svg",
15
+ mimeType: "image/svg+xml",
16
+ sizes: ["512x512"],
17
+ },
18
+ ],
19
+
20
+ // The MCP server is by default served at /mcp, to customise
21
+ // basePath: "/mcp",
22
+
23
+ // mcp-use has 1 line adapter for OAuth, import from mcp-use/oauth/*
24
+ // oauth: oauthClerkProvider(), // zero-config via MCP_USE_OAUTH_CLERK_FRONTEND_API_URL, import from mcp-use/oauth/*
25
+
26
+ // When OAuth is on, the HTML landing page (/mcp) is protected by default, set to true to keep the landing page public while /mcp stays bearer-protected.
27
+ // publicLandingPage: true,
16
28
  });
17
29
 
18
- server.tool(
30
+ // TOOLS
31
+
32
+ // tool inputs are zod schemas. Important: set decriptions for the model to understand the input.
33
+ const showAppInputSchema = z.object({
34
+ appName: z
35
+ .string()
36
+ .optional()
37
+ .describe(
38
+ "Optional title shown in the MCP App card instead of the default"
39
+ ),
40
+ });
41
+
42
+ // tool outputs are zod schemas. Important: set descriptions for the model to understand the output.
43
+ const showAppOutputSchema = z.object({
44
+ message: z.string().describe("Message to display in the MCP App"),
45
+ });
46
+
47
+ export const showApp = server.tool(
19
48
  {
20
- name: "search-fruits",
21
- description: "Search the fruit catalog",
22
- inputSchema: z.object({ query: z.string().optional() }),
23
- outputSchema,
49
+ name: "show-app", // Unique tool id on the wire.
50
+ title: "Show MCP App", // Short label in inspector and client UIs (falls back to name).
51
+ description: "Display the MCP App starter view", // LLM-facing summary of what the tool does.
52
+ inputSchema: showAppInputSchema, // Validated before the handler runs; .describe() text becomes LLM hints.
53
+ outputSchema: showAppOutputSchema, // Required when binding a view — the view reads structuredContent typed by this.
24
54
  view: {
25
- name: "product-search-result",
26
- description: "Fruit search results",
27
- prefersBorder: true,
55
+ name: "my-view", // directory under views/
56
+ description: "MCP App starter card with interactive demo hooks",
57
+ prefersBorder: false, // ask the host to skip a card border around the view
58
+ csp: {
59
+ resourceDomains: [
60
+ "https://fonts.googleapis.com",
61
+ "https://fonts.gstatic.com",
62
+ ],
63
+ },
64
+ },
65
+ // Behavioral hints for clients (readOnly / destructive / open-world).
66
+ annotations: {
67
+ readOnlyHint: true,
68
+ destructiveHint: false,
69
+ openWorldHint: false,
70
+ },
71
+ },
72
+ async ({ appName }, ctx) => {
73
+ // Brief delay so the pending skeleton is visible during dev/inspector demos.
74
+ await new Promise((resolve) => setTimeout(resolve, 1500));
75
+
76
+ console.log(`The app name was: ${appName}`);
77
+ const data = { message: `The MCP App starter for ${appName} is ready!` };
78
+
79
+ return {
80
+ content: [{ type: "text", text: data.message }],
81
+ structuredContent: data,
82
+ };
83
+ }
84
+ );
85
+
86
+ // say-hello — plain tool (no view); used by the Say Hello button in my-view
87
+ const sayHelloInputSchema = z.object({
88
+ name: z.string().describe("Name to greet"),
89
+ });
90
+
91
+ const sayHelloOutputSchema = z.object({
92
+ greeting: z.string().describe("Greeting to display"),
93
+ });
94
+
95
+ export const sayHello = server.tool(
96
+ {
97
+ name: "say-hello",
98
+ title: "Say hello",
99
+ description: "Returns a greeting for the Say Hello button demo",
100
+ inputSchema: sayHelloInputSchema,
101
+ outputSchema: sayHelloOutputSchema,
102
+ annotations: {
103
+ readOnlyHint: true,
104
+ destructiveHint: false,
105
+ openWorldHint: false,
28
106
  },
29
- annotations: { readOnlyHint: true, openWorldHint: false },
30
107
  },
31
- async ({ query = "" }) => {
32
- const items = fruits
33
- .filter((fruit) => fruit.includes(query.toLowerCase()))
34
- .map((fruit) => ({ id: fruit, name: fruit }));
35
- const data = { query, items };
108
+ async ({ name }) => {
109
+ const data = { greeting: `Hello, ${name}!` };
36
110
  return {
37
- content: [{ type: "text", text: `Found ${items.length} fruits.` }],
111
+ content: [{ type: "text", text: data.greeting }],
38
112
  structuredContent: data,
39
113
  };
40
114
  }
@@ -0,0 +1,10 @@
1
+ // Generated by mcp-use. Do not edit.
2
+ declare module "*.css";
3
+
4
+ declare module "mcp-use/react" {
5
+ interface Register {
6
+ tools: typeof import("./index.js");
7
+ }
8
+ }
9
+
10
+ export {};
@@ -1,20 +1,22 @@
1
1
  {
2
2
  "name": "mcp-use server",
3
3
  "type": "module",
4
+ "engines": {
5
+ "node": ">=22.22.2"
6
+ },
4
7
  "version": "1.0.0",
5
- "description": "MCP server with a standard MCP Apps view",
6
- "author": "",
8
+ "description": "an mcp-use server",
9
+ "author": "mcp-use",
7
10
  "license": "MIT",
11
+ "homepage": "https://github.com/mcp-use/mcp-use",
8
12
  "keywords": [
9
13
  "mcp",
10
- "server",
11
- "starter",
12
- "tools",
13
- "resources",
14
- "prompts",
15
- "ui",
16
- "react",
17
- "mcp-apps"
14
+ "mcp-server",
15
+ "chatgpt plugins",
16
+ "claude connector",
17
+ "mcp-app",
18
+ "mcp-use",
19
+ "manufact"
18
20
  ],
19
21
  "main": "dist/index.js",
20
22
  "scripts": {
@@ -22,18 +24,18 @@
22
24
  "dev": "mcp-use dev",
23
25
  "start": "mcp-use start",
24
26
  "deploy": "mcp-use deploy",
25
- "typecheck": "tsc --noEmit"
27
+ "typecheck": "mcp-use typecheck"
26
28
  },
27
29
  "dependencies": {
28
30
  "mcp-use": "workspace:*",
29
- "react": "^19.2.4",
30
- "react-dom": "^19.2.4",
31
+ "react": "^19.2.7",
32
+ "react-dom": "^19.2.7",
31
33
  "zod": "^4.4.3"
32
34
  },
33
35
  "devDependencies": {
34
- "@types/node": "^24.13.2",
36
+ "@types/node": "^22.20.0",
35
37
  "@types/react": "^19.2.17",
36
38
  "@types/react-dom": "^19.2.3",
37
- "typescript": "7.0.1-rc"
39
+ "typescript": "^7.0.2"
38
40
  }
39
41
  }
@@ -0,0 +1,8 @@
1
+ <svg width="2476" height="480" viewBox="0 0 2476 480" fill="none" xmlns="http://www.w3.org/2000/svg">
2
+ <path d="M793.5 245.5C793.5 218.167 790 198.5 783 186.5C776.333 174.167 763.667 168 745 168C738.333 168 731 168.5 723 169.5C715 170.5 709 171.333 705 172V383H644.5V130C656.167 126.667 671.333 123.5 690 120.5C709 117.5 729 116 750 116C768 116 782.667 118.333 794 123C805.667 127.667 815.333 133.833 823 141.5C826.667 138.833 831.333 136 837 133C842.667 130 849 127.333 856 125C863 122.333 870.333 120.167 878 118.5C886 116.833 894 116 902 116C922.333 116 939 119 952 125C965.333 130.667 975.667 138.833 983 149.5C990.667 159.833 995.833 172.5 998.5 187.5C1001.5 202.167 1003 218.333 1003 236V383H942.5V245.5C942.5 218.167 939.167 198.5 932.5 186.5C925.833 174.167 913 168 894 168C884.333 168 875.167 169.667 866.5 173C857.833 176 851.333 179 847 182C849.667 190.333 851.5 199.167 852.5 208.5C853.5 217.833 854 227.833 854 238.5V383H793.5V245.5ZM1052.62 252.5C1052.62 233.167 1055.62 215.167 1061.62 198.5C1067.62 181.5 1076.12 166.833 1087.12 154.5C1098.45 142.167 1112.12 132.5 1128.12 125.5C1144.12 118.5 1162.12 115 1182.12 115C1206.78 115 1230.12 119.5 1252.12 128.5L1239.12 178C1232.12 175 1224.12 172.5 1215.12 170.5C1206.45 168.5 1197.12 167.5 1187.12 167.5C1163.45 167.5 1145.45 175 1133.12 190C1120.78 204.667 1114.62 225.5 1114.62 252.5C1114.62 278.5 1120.45 299.167 1132.12 314.5C1143.78 329.5 1163.45 337 1191.12 337C1201.45 337 1211.62 336 1221.62 334C1231.62 332 1240.28 329.5 1247.62 326.5L1256.12 376.5C1249.45 379.833 1239.28 382.833 1225.62 385.5C1212.28 388.167 1198.45 389.5 1184.12 389.5C1161.78 389.5 1142.28 386.167 1125.62 379.5C1109.28 372.5 1095.62 363 1084.62 351C1073.95 338.667 1065.95 324.167 1060.62 307.5C1055.28 290.5 1052.62 272.167 1052.62 252.5ZM1466.46 253.5C1466.46 226.5 1460.29 205.5 1447.96 190.5C1435.96 175.5 1416.46 168 1389.46 168C1383.79 168 1377.79 168.333 1371.46 169C1365.46 169.333 1359.46 170.333 1353.46 172V321.5C1358.79 325.167 1365.79 328.667 1374.46 332C1383.46 335 1392.96 336.5 1402.96 336.5C1424.96 336.5 1440.96 329 1450.96 314C1461.29 299 1466.46 278.833 1466.46 253.5ZM1528.46 252.5C1528.46 272.5 1525.79 290.833 1520.46 307.5C1515.46 324.167 1508.12 338.5 1498.46 350.5C1488.79 362.5 1476.62 371.833 1461.96 378.5C1447.62 385.167 1431.29 388.5 1412.96 388.5C1400.62 388.5 1389.29 387 1378.96 384C1368.62 381 1360.12 377.667 1353.46 374V475.5H1292.96V130C1305.29 126.667 1320.46 123.5 1338.46 120.5C1356.46 117.5 1375.46 116 1395.46 116C1416.12 116 1434.62 119.167 1450.96 125.5C1467.29 131.833 1481.12 141 1492.46 153C1504.12 164.667 1512.96 179 1518.96 196C1525.29 212.667 1528.46 231.5 1528.46 252.5ZM1553.81 212.5H1688.31V268H1553.81V212.5ZM1943.02 375C1931.36 378 1916.19 381 1897.52 384C1878.86 387.333 1858.36 389 1836.02 389C1815.02 389 1797.36 386 1783.02 380C1769.02 374 1757.69 365.667 1749.02 355C1740.69 344.333 1734.69 331.667 1731.02 317C1727.36 302 1725.52 285.667 1725.52 268V121.5H1786.02V258.5C1786.02 286.5 1790.02 306.5 1798.02 318.5C1806.36 330.5 1820.69 336.5 1841.02 336.5C1848.36 336.5 1856.02 336.167 1864.02 335.5C1872.36 334.833 1878.52 334 1882.52 333V121.5H1943.02V375ZM2075.53 339.5C2091.53 339.5 2103.19 337.667 2110.53 334C2117.86 330 2121.53 323.333 2121.53 314C2121.53 305.333 2117.53 298.167 2109.53 292.5C2101.86 286.833 2089.03 280.667 2071.03 274C2060.03 270 2049.86 265.833 2040.53 261.5C2031.53 256.833 2023.69 251.5 2017.03 245.5C2010.36 239.5 2005.03 232.333 2001.03 224C1997.36 215.333 1995.53 204.833 1995.53 192.5C1995.53 168.5 2004.36 149.667 2022.03 136C2039.69 122 2063.69 115 2094.03 115C2109.36 115 2124.03 116.5 2138.03 119.5C2152.03 122.167 2162.53 124.833 2169.53 127.5L2158.53 176.5C2151.86 173.5 2143.36 170.833 2133.03 168.5C2122.69 165.833 2110.69 164.5 2097.03 164.5C2084.69 164.5 2074.69 166.667 2067.03 171C2059.36 175 2055.53 181.333 2055.53 190C2055.53 194.333 2056.19 198.167 2057.53 201.5C2059.19 204.833 2061.86 208 2065.53 211C2069.19 213.667 2074.03 216.5 2080.03 219.5C2086.03 222.167 2093.36 225 2102.03 228C2116.36 233.333 2128.53 238.667 2138.53 244C2148.53 249 2156.69 254.833 2163.03 261.5C2169.69 267.833 2174.53 275.167 2177.53 283.5C2180.53 291.833 2182.03 301.833 2182.03 313.5C2182.03 338.5 2172.69 357.5 2154.03 370.5C2135.69 383.167 2109.36 389.5 2075.03 389.5C2052.03 389.5 2033.53 387.5 2019.53 383.5C2005.53 379.833 1995.69 376.833 1990.03 374.5L2000.53 324C2009.53 327.667 2020.19 331.167 2032.53 334.5C2045.19 337.833 2059.53 339.5 2075.53 339.5ZM2214.04 253.5C2214.04 230.5 2217.38 210.333 2224.04 193C2231.04 175.667 2240.21 161.333 2251.54 150C2262.88 138.333 2275.88 129.667 2290.54 124C2305.21 118 2320.21 115 2335.54 115C2371.54 115 2399.54 126.167 2419.54 148.5C2439.88 170.833 2450.04 204.167 2450.04 248.5C2450.04 251.833 2449.88 255.667 2449.54 260C2449.54 264 2449.38 267.667 2449.04 271H2276.54C2278.21 292 2285.54 308.333 2298.54 320C2311.88 331.333 2331.04 337 2356.04 337C2370.71 337 2384.04 335.667 2396.04 333C2408.38 330.333 2418.04 327.5 2425.04 324.5L2433.04 374C2429.71 375.667 2425.04 377.5 2419.04 379.5C2413.38 381.167 2406.71 382.667 2399.04 384C2391.71 385.667 2383.71 387 2375.04 388C2366.38 389 2357.54 389.5 2348.54 389.5C2325.54 389.5 2305.54 386.167 2288.54 379.5C2271.54 372.5 2257.54 363 2246.54 351C2235.54 338.667 2227.38 324.333 2222.04 308C2216.71 291.333 2214.04 273.167 2214.04 253.5ZM2389.54 226.5C2389.54 218.167 2388.38 210.333 2386.04 203C2383.71 195.333 2380.21 188.833 2375.54 183.5C2371.21 177.833 2365.71 173.5 2359.04 170.5C2352.71 167.167 2345.04 165.5 2336.04 165.5C2326.71 165.5 2318.54 167.333 2311.54 171C2304.54 174.333 2298.54 178.833 2293.54 184.5C2288.88 190.167 2285.21 196.667 2282.54 204C2279.88 211.333 2278.04 218.833 2277.04 226.5H2389.54Z" fill="black"/>
3
+ <g transform="scale(0.96)">
4
+ <path d="M105.933 0C164.437 0.000116002 211.865 47.607 211.865 106.333C211.865 131.829 210.493 158.403 221.068 181.602L228.975 198.947C243.584 230.997 269.265 256.7 301.303 271.336L316.155 278.121C340.142 289.079 367.694 287.335 394.066 287.335C452.571 287.335 499.999 334.942 499.999 393.668C499.999 452.394 452.571 500.001 394.066 500.001C335.562 500.001 288.134 452.394 288.134 393.668C288.134 368.974 289.24 343.275 278.992 320.807L270.586 302.38C255.948 270.29 230.214 244.565 198.118 229.939L180.164 221.758C157.282 211.331 131.078 212.666 105.933 212.666C47.4278 212.666 4.86252e-05 165.059 0 106.333C0 47.607 47.4278 0 105.933 0Z" fill="black"/>
5
+ <circle cx="100.426" cy="399.575" r="100.426" fill="black"/>
6
+ <path d="M500 100.426C500 155.889 455.037 200.851 399.574 200.851C344.11 200.851 299.148 155.889 299.148 100.426C299.148 44.962 344.11 0 399.574 0C455.037 0 500 44.962 500 100.426Z" fill="black"/>
7
+ </g>
8
+ </svg>
@@ -10,9 +10,15 @@
10
10
  "forceConsistentCasingInFileNames": true,
11
11
  "skipLibCheck": true,
12
12
  "noEmit": true,
13
- "noUncheckedIndexedAccess": true,
14
- "exactOptionalPropertyTypes": true
13
+ "noUncheckedSideEffectImports": false
15
14
  },
16
- "include": ["index.ts", "src/**/*", "resources/**/*"],
15
+ "include": [
16
+ "index.ts",
17
+ "mcp-env.d.ts",
18
+ "src/**/*",
19
+ "views/**/*",
20
+ "server.ts",
21
+ ".mcp-use/**/*.d.ts"
22
+ ],
17
23
  "exclude": ["node_modules", "dist"]
18
24
  }
@@ -0,0 +1,299 @@
1
+ import { useEffect, useRef } from "react";
2
+
3
+ // Hero mesh palette (light landing surfaces)
4
+ const MESH_COLORS = ["#e0eaff", "#f9ffbd", "#dedede", "#ffffff"];
5
+ const HERO_MESH_SPEED = 1.15;
6
+ const HERO_MESH_SWIRL = 0.95;
7
+ const ORB_MAX_WIDTH_PX = 800;
8
+
9
+ const VERT = `#version 300 es
10
+ precision mediump float;
11
+ in vec4 a_position;
12
+ uniform vec2 u_resolution;
13
+ out vec2 v_objectUV;
14
+ void main(){
15
+ gl_Position=a_position;
16
+ vec2 uv=a_position.xy*0.5;
17
+ float box=min(u_resolution.x,u_resolution.y);
18
+ vec2 scale=box>0.0?u_resolution/box:vec2(1.);
19
+ v_objectUV=uv*scale;
20
+ }`;
21
+
22
+ const FRAG = `#version 300 es
23
+ precision mediump float;
24
+ uniform float u_time;
25
+ uniform vec4 u_colors[4];
26
+ uniform float u_colorsCount;
27
+ uniform float u_distortion;
28
+ uniform float u_swirl;
29
+ uniform float u_grainMixer;
30
+ uniform float u_grainOverlay;
31
+ in vec2 v_objectUV;
32
+ out vec4 fragColor;
33
+ vec2 rotate(vec2 uv,float th){return mat2(cos(th),sin(th),-sin(th),cos(th))*uv;}
34
+ float hash21(vec2 p){
35
+ p=fract(p*vec2(0.3183099,0.3678794))+0.1;
36
+ p+=dot(p,p+19.19);
37
+ return fract(p.x*p.y);
38
+ }
39
+ float valueNoise(vec2 st){
40
+ vec2 i=floor(st);vec2 f=fract(st);
41
+ float a=hash21(i),b=hash21(i+vec2(1.,0.)),c=hash21(i+vec2(0.,1.)),d=hash21(i+vec2(1.,1.));
42
+ vec2 u=f*f*(3.-2.*f);
43
+ return mix(mix(a,b,u.x),mix(c,d,u.x),u.y);
44
+ }
45
+ vec2 getPosition(int i,float t){
46
+ float a=float(i)*.37,b=.6+fract(float(i)/3.)*.9,c=.8+fract(float(i+1)/4.);
47
+ float x=sin(t*b+a),y=cos(t*c+a*1.5);
48
+ return .5+.5*vec2(x,y);
49
+ }
50
+ void main(){
51
+ vec2 uv=v_objectUV;uv+=.5;
52
+ vec2 grainUV=uv*1000.;
53
+ float grain=valueNoise(grainUV);
54
+ float mixerGrain=.4*u_grainMixer*(grain-.5);
55
+ float t=.5*(u_time+41.5);
56
+ float radius=smoothstep(0.,1.,length(uv-.5));
57
+ float center=1.-radius;
58
+ for(float i=1.;i<=2.;i++){
59
+ uv.x+=u_distortion*center/i*sin(t+i*.4*smoothstep(0.,1.,uv.y))*cos(.2*t+i*2.4*smoothstep(0.,1.,uv.y));
60
+ uv.y+=u_distortion*center/i*cos(t+i*2.*smoothstep(0.,1.,uv.x));
61
+ }
62
+ vec2 uvR=uv-vec2(.5);
63
+ float angle=3.*u_swirl*radius;
64
+ uvR=rotate(uvR,-angle);uvR+=vec2(.5);
65
+ vec3 color=vec3(0.);float opacity=0.,totalWeight=0.;
66
+ for(int i=0;i<4;i++){
67
+ if(i>=int(u_colorsCount))break;
68
+ vec2 pos=getPosition(i,t)+mixerGrain;
69
+ vec3 cf=u_colors[i].rgb*u_colors[i].a;
70
+ float opacityF=u_colors[i].a;
71
+ float dist=length(uvR-pos);
72
+ dist=pow(dist,3.5);
73
+ float w=1./(dist+1e-3);
74
+ color+=cf*w;opacity+=opacityF*w;totalWeight+=w;
75
+ }
76
+ color/=max(1e-4,totalWeight);
77
+ opacity/=max(1e-4,totalWeight);
78
+ float grainO=valueNoise(rotate(grainUV,1.)+vec2(3.));
79
+ grainO=mix(grainO,valueNoise(rotate(grainUV,2.)+vec2(-1.)),.5);
80
+ grainO=pow(grainO,1.3);
81
+ float grainOV=grainO*2.-1.;
82
+ vec3 grainOC=vec3(step(0.,grainOV));
83
+ float grainOS=u_grainOverlay*abs(grainOV);
84
+ grainOS=pow(grainOS,.8);
85
+ color=mix(color,grainOC,.35*grainOS);
86
+ opacity+=.5*grainOS;
87
+ fragColor=vec4(color,clamp(opacity,0.,1.));
88
+ }`;
89
+
90
+ function hexToRgba(hex: string): [number, number, number, number] {
91
+ return [
92
+ parseInt(hex.slice(1, 3), 16) / 255,
93
+ parseInt(hex.slice(3, 5), 16) / 255,
94
+ parseInt(hex.slice(5, 7), 16) / 255,
95
+ 1,
96
+ ];
97
+ }
98
+
99
+ /** WebGL2 mesh gradient canvas (self-contained; no external deps). */
100
+ function MeshGradientCanvas({
101
+ className,
102
+ colors = MESH_COLORS,
103
+ distortion = 0.8,
104
+ swirl = 0.1,
105
+ grainMixer = 0,
106
+ grainOverlay = 0.2,
107
+ speed = 1,
108
+ }: {
109
+ className?: string;
110
+ colors?: string[];
111
+ distortion?: number;
112
+ swirl?: number;
113
+ grainMixer?: number;
114
+ grainOverlay?: number;
115
+ speed?: number;
116
+ }) {
117
+ const canvasRef = useRef<HTMLCanvasElement>(null);
118
+ const wrapRef = useRef<HTMLDivElement>(null);
119
+ const propsRef = useRef({
120
+ colors,
121
+ distortion,
122
+ swirl,
123
+ grainMixer,
124
+ grainOverlay,
125
+ speed,
126
+ });
127
+ propsRef.current = {
128
+ colors,
129
+ distortion,
130
+ swirl,
131
+ grainMixer,
132
+ grainOverlay,
133
+ speed,
134
+ };
135
+
136
+ useEffect(() => {
137
+ const canvas = canvasRef.current;
138
+ const wrap = wrapRef.current;
139
+ if (!canvas || !wrap) return;
140
+
141
+ const gl = canvas.getContext("webgl2");
142
+ if (!gl) return;
143
+
144
+ const compileShader = (type: number, src: string) => {
145
+ const shader = gl.createShader(type);
146
+ if (!shader) return null;
147
+ gl.shaderSource(shader, src);
148
+ gl.compileShader(shader);
149
+ if (!gl.getShaderParameter(shader, gl.COMPILE_STATUS)) {
150
+ console.error(gl.getShaderInfoLog(shader));
151
+ return null;
152
+ }
153
+ return shader;
154
+ };
155
+
156
+ const vs = compileShader(gl.VERTEX_SHADER, VERT);
157
+ const fs = compileShader(gl.FRAGMENT_SHADER, FRAG);
158
+ if (!vs || !fs) return;
159
+
160
+ const prog = gl.createProgram();
161
+ if (!prog) return;
162
+ gl.attachShader(prog, vs);
163
+ gl.attachShader(prog, fs);
164
+ gl.linkProgram(prog);
165
+ if (!gl.getProgramParameter(prog, gl.LINK_STATUS)) {
166
+ console.error(gl.getProgramInfoLog(prog));
167
+ return;
168
+ }
169
+ gl.useProgram(prog);
170
+
171
+ const posLoc = gl.getAttribLocation(prog, "a_position");
172
+ const buf = gl.createBuffer();
173
+ gl.bindBuffer(gl.ARRAY_BUFFER, buf);
174
+ gl.bufferData(
175
+ gl.ARRAY_BUFFER,
176
+ new Float32Array([-1, -1, 1, -1, -1, 1, -1, 1, 1, -1, 1, 1]),
177
+ gl.STATIC_DRAW
178
+ );
179
+ gl.enableVertexAttribArray(posLoc);
180
+ gl.vertexAttribPointer(posLoc, 2, gl.FLOAT, false, 0, 0);
181
+
182
+ const uTime = gl.getUniformLocation(prog, "u_time");
183
+ const uResolution = gl.getUniformLocation(prog, "u_resolution");
184
+ const uColors = [0, 1, 2, 3].map((i) =>
185
+ gl.getUniformLocation(prog, `u_colors[${i}]`)
186
+ );
187
+ const uColorsCount = gl.getUniformLocation(prog, "u_colorsCount");
188
+ const uDistortion = gl.getUniformLocation(prog, "u_distortion");
189
+ const uSwirl = gl.getUniformLocation(prog, "u_swirl");
190
+ const uGrainMixer = gl.getUniformLocation(prog, "u_grainMixer");
191
+ const uGrainOverlay = gl.getUniformLocation(prog, "u_grainOverlay");
192
+
193
+ let frameMs = 0;
194
+ let lastNow = performance.now();
195
+ let raf = 0;
196
+
197
+ const resize = () => {
198
+ const dpr = Math.min(2, window.devicePixelRatio || 1);
199
+ canvas.width = Math.floor(wrap.clientWidth * dpr);
200
+ canvas.height = Math.floor(wrap.clientHeight * dpr);
201
+ gl.viewport(0, 0, canvas.width, canvas.height);
202
+ };
203
+
204
+ const draw = (now: number) => {
205
+ if (document.hidden) {
206
+ lastNow = now;
207
+ raf = requestAnimationFrame(draw);
208
+ return;
209
+ }
210
+ const {
211
+ colors: currentColors,
212
+ distortion: currentDistortion,
213
+ swirl: currentSwirl,
214
+ grainMixer: currentGrainMixer,
215
+ grainOverlay: currentGrainOverlay,
216
+ speed: currentSpeed,
217
+ } = propsRef.current;
218
+ const dt = now - lastNow;
219
+ lastNow = now;
220
+ frameMs += dt * currentSpeed;
221
+ const rgbaColors = currentColors.map(hexToRgba);
222
+ gl.uniform1f(uTime, frameMs * 1e-3);
223
+ gl.uniform2f(uResolution, canvas.width, canvas.height);
224
+ for (let i = 0; i < 4; i++) {
225
+ const loc = uColors[i];
226
+ if (loc) gl.uniform4fv(loc, rgbaColors[i] ?? [1, 1, 1, 1]);
227
+ }
228
+ gl.uniform1f(uColorsCount, rgbaColors.length);
229
+ gl.uniform1f(uDistortion, currentDistortion);
230
+ gl.uniform1f(uSwirl, currentSwirl);
231
+ gl.uniform1f(uGrainMixer, currentGrainMixer);
232
+ gl.uniform1f(uGrainOverlay, currentGrainOverlay);
233
+ gl.drawArrays(gl.TRIANGLES, 0, 6);
234
+ raf = requestAnimationFrame(draw);
235
+ };
236
+
237
+ resize();
238
+ const ro = new ResizeObserver(resize);
239
+ ro.observe(wrap);
240
+ raf = requestAnimationFrame(draw);
241
+
242
+ return () => {
243
+ cancelAnimationFrame(raf);
244
+ ro.disconnect();
245
+ };
246
+ }, []);
247
+
248
+ return (
249
+ <div ref={wrapRef} className={className}>
250
+ <canvas ref={canvasRef} className="h-full w-full" aria-hidden />
251
+ </div>
252
+ );
253
+ }
254
+
255
+ function MeshOrbContent() {
256
+ return (
257
+ <>
258
+ <div className="absolute inset-0 bg-[#edf2ff]" />
259
+ <MeshGradientCanvas
260
+ className="absolute inset-0 h-full w-full"
261
+ colors={MESH_COLORS}
262
+ distortion={0.8}
263
+ swirl={HERO_MESH_SWIRL}
264
+ grainMixer={0}
265
+ grainOverlay={0.3}
266
+ speed={HERO_MESH_SPEED}
267
+ />
268
+ </>
269
+ );
270
+ }
271
+
272
+ /** Mesh disc peeking from the bottom — 80% of card width, capped at 800px. */
273
+ export function HeroMeshOrb({ flow = false }: { flow?: boolean }) {
274
+ if (flow) {
275
+ return (
276
+ <div
277
+ className="relative z-0 mt-2 w-full overflow-hidden pb-[22%]"
278
+ aria-hidden
279
+ >
280
+ <div
281
+ className="absolute bottom-0 left-1/2 aspect-square w-full -translate-x-1/2 translate-y-[78%] overflow-hidden rounded-full opacity-90"
282
+ style={{ maxWidth: ORB_MAX_WIDTH_PX }}
283
+ >
284
+ <MeshOrbContent />
285
+ </div>
286
+ </div>
287
+ );
288
+ }
289
+
290
+ return (
291
+ <div
292
+ className="pointer-events-none absolute bottom-0 left-1/2 z-0 aspect-square w-[80%] -translate-x-1/2 translate-y-[78%] overflow-hidden rounded-full opacity-90"
293
+ style={{ maxWidth: ORB_MAX_WIDTH_PX }}
294
+ aria-hidden
295
+ >
296
+ <MeshOrbContent />
297
+ </div>
298
+ );
299
+ }
@@ -0,0 +1,75 @@
1
+ import { useRef, useState, type ReactNode } from "react";
2
+
3
+ /** Hover/focus tooltip clamped to the nearest [data-card-shell] ancestor. */
4
+ export function Tooltip({
5
+ label,
6
+ children,
7
+ multiline = false,
8
+ }: {
9
+ label: string;
10
+ children: ReactNode;
11
+ multiline?: boolean;
12
+ }) {
13
+ const wrapRef = useRef<HTMLSpanElement>(null);
14
+ const tipRef = useRef<HTMLSpanElement>(null);
15
+ const [open, setOpen] = useState(false);
16
+ const [pos, setPos] = useState({ left: 0, maxWidth: 320 });
17
+
18
+ const show = () => {
19
+ const wrap = wrapRef.current;
20
+ const tip = tipRef.current;
21
+ const card = wrap?.closest("[data-card-shell]");
22
+ if (!wrap || !tip || !card) {
23
+ setOpen(true);
24
+ return;
25
+ }
26
+ const pad = 12;
27
+ const cardR = card.getBoundingClientRect();
28
+ const wrapR = wrap.getBoundingClientRect();
29
+ const maxWidth = Math.min(320, Math.max(160, cardR.width - pad * 2));
30
+ Object.assign(tip.style, {
31
+ maxWidth: `${maxWidth}px`,
32
+ visibility: "hidden",
33
+ opacity: "1",
34
+ });
35
+ const w = tip.offsetWidth;
36
+ tip.style.visibility = "";
37
+ tip.style.opacity = "";
38
+ const left =
39
+ Math.max(
40
+ cardR.left + pad,
41
+ Math.min(wrapR.left + wrapR.width / 2 - w / 2, cardR.right - pad - w)
42
+ ) - wrapR.left;
43
+ setPos({ left, maxWidth });
44
+ setOpen(true);
45
+ };
46
+
47
+ return (
48
+ <span
49
+ ref={wrapRef}
50
+ className="relative inline-flex"
51
+ onMouseEnter={show}
52
+ onMouseLeave={() => setOpen(false)}
53
+ onFocus={show}
54
+ onBlur={() => setOpen(false)}
55
+ >
56
+ {children}
57
+ <span
58
+ ref={tipRef}
59
+ role="tooltip"
60
+ style={{
61
+ left: pos.left,
62
+ maxWidth: pos.maxWidth,
63
+ opacity: open ? 1 : 0,
64
+ }}
65
+ className={`pointer-events-none absolute top-full z-30 mt-1.5 w-max rounded-lg bg-zinc-900 px-3 py-2 text-[11px] font-medium text-white transition-opacity duration-200 dark:bg-zinc-800 dark:text-neutral-100 ${
66
+ multiline
67
+ ? "whitespace-normal text-left leading-snug"
68
+ : "whitespace-nowrap"
69
+ }`}
70
+ >
71
+ {label}
72
+ </span>
73
+ </span>
74
+ );
75
+ }