framer-code-link 0.7.0 → 0.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/dist/index.mjs +139 -12
- package/package.json +3 -2
- package/skills/SKILL.md +133 -0
- package/skills/references/EXAMPLES.md +869 -0
- package/skills/references/PROPERTY_CONTROLS.md +715 -0
- package/skills/references/PROPERTY_CONTROL_GUIDE.md +332 -0
- package/skills/references/PROPERTY_CONTROL_TYPES.md +488 -0
package/dist/index.mjs
CHANGED
|
@@ -2,14 +2,15 @@
|
|
|
2
2
|
import { createRequire } from "node:module";
|
|
3
3
|
import { Command } from "commander";
|
|
4
4
|
import fs from "fs/promises";
|
|
5
|
-
import { WebSocketServer } from "ws";
|
|
6
5
|
import path from "path";
|
|
6
|
+
import { WebSocketServer } from "ws";
|
|
7
7
|
import { createHash } from "crypto";
|
|
8
8
|
import { setupTypeAcquisition } from "@typescript/ata";
|
|
9
9
|
import ts from "typescript";
|
|
10
|
+
import { fileURLToPath } from "node:url";
|
|
10
11
|
import chokidar from "chokidar";
|
|
11
12
|
|
|
12
|
-
//#region
|
|
13
|
+
//#region \0rolldown/runtime.js
|
|
13
14
|
var __create = Object.create;
|
|
14
15
|
var __defProp = Object.defineProperty;
|
|
15
16
|
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
@@ -1042,6 +1043,113 @@ function extractPackageFromUrl(url) {
|
|
|
1042
1043
|
return /\/(@?[^@/]+(?:\/[^@/]+)?)/.exec(url)?.[1] ?? null;
|
|
1043
1044
|
}
|
|
1044
1045
|
|
|
1046
|
+
//#endregion
|
|
1047
|
+
//#region src/helpers/skills.ts
|
|
1048
|
+
/**
|
|
1049
|
+
* Agent Skills installer — copies the skill into the project directory
|
|
1050
|
+
* and symlinks it into agent-specific paths.
|
|
1051
|
+
*/
|
|
1052
|
+
/** Agent-specific skill directories that get symlinked to the canonical .skills/ location */
|
|
1053
|
+
const AGENT_SKILL_DIRS = [
|
|
1054
|
+
".agents/skills",
|
|
1055
|
+
".claude/skills",
|
|
1056
|
+
".cursor/skills"
|
|
1057
|
+
];
|
|
1058
|
+
/**
|
|
1059
|
+
* Read the skill name from the SKILL.md frontmatter.
|
|
1060
|
+
*/
|
|
1061
|
+
async function readSkillName(sourceDir) {
|
|
1062
|
+
const content = await fs.readFile(path.join(sourceDir, "SKILL.md"), "utf-8");
|
|
1063
|
+
const match = /^name:\s*(.+)$/m.exec(content);
|
|
1064
|
+
if (!match) throw new Error("Could not read skill name from SKILL.md frontmatter");
|
|
1065
|
+
return match[1].trim();
|
|
1066
|
+
}
|
|
1067
|
+
/**
|
|
1068
|
+
* Recursively collect all file paths relative to a directory.
|
|
1069
|
+
*/
|
|
1070
|
+
async function collectFiles(dir, base = "") {
|
|
1071
|
+
const entries = await fs.readdir(dir, { withFileTypes: true });
|
|
1072
|
+
const files = [];
|
|
1073
|
+
for (const entry of entries) {
|
|
1074
|
+
const rel = base ? `${base}/${entry.name}` : entry.name;
|
|
1075
|
+
if (entry.isDirectory()) files.push(...await collectFiles(path.join(dir, entry.name), rel));
|
|
1076
|
+
else files.push(rel);
|
|
1077
|
+
}
|
|
1078
|
+
return files;
|
|
1079
|
+
}
|
|
1080
|
+
/**
|
|
1081
|
+
* Install the agent skill into the project.
|
|
1082
|
+
* Writes the canonical skill to .skills/<name>/ and symlinks
|
|
1083
|
+
* into agent-specific directories.
|
|
1084
|
+
*/
|
|
1085
|
+
async function installSkills(projectDir) {
|
|
1086
|
+
const sourceDir = await findSkillsSourceDir();
|
|
1087
|
+
if (!sourceDir) {
|
|
1088
|
+
debug("Could not locate skills source files, skipping skill installation");
|
|
1089
|
+
return;
|
|
1090
|
+
}
|
|
1091
|
+
const skillName = await readSkillName(sourceDir);
|
|
1092
|
+
const canonicalDir = path.join(projectDir, ".skills", skillName);
|
|
1093
|
+
const skillMdPath = path.join(canonicalDir, "SKILL.md");
|
|
1094
|
+
try {
|
|
1095
|
+
await fs.access(skillMdPath);
|
|
1096
|
+
debug("Agent skills already installed");
|
|
1097
|
+
return;
|
|
1098
|
+
} catch {}
|
|
1099
|
+
const files = await collectFiles(sourceDir);
|
|
1100
|
+
for (const file of files) {
|
|
1101
|
+
const src = path.join(sourceDir, file);
|
|
1102
|
+
const dest = path.join(canonicalDir, file);
|
|
1103
|
+
try {
|
|
1104
|
+
await fs.mkdir(path.dirname(dest), { recursive: true });
|
|
1105
|
+
await fs.copyFile(src, dest);
|
|
1106
|
+
} catch (err) {
|
|
1107
|
+
debug(`Failed to copy skill file ${file}`, err);
|
|
1108
|
+
return;
|
|
1109
|
+
}
|
|
1110
|
+
}
|
|
1111
|
+
debug(`Installed agent skill to .skills/${skillName}/`);
|
|
1112
|
+
for (const agentDir of AGENT_SKILL_DIRS) {
|
|
1113
|
+
const linkDir = path.join(projectDir, agentDir);
|
|
1114
|
+
const linkPath = path.join(linkDir, skillName);
|
|
1115
|
+
const relativeTarget = path.relative(linkDir, canonicalDir);
|
|
1116
|
+
try {
|
|
1117
|
+
await fs.mkdir(linkDir, { recursive: true });
|
|
1118
|
+
try {
|
|
1119
|
+
const stat = await fs.lstat(linkPath);
|
|
1120
|
+
if (stat.isSymbolicLink() || stat.isDirectory()) await fs.rm(linkPath, { recursive: true });
|
|
1121
|
+
} catch {}
|
|
1122
|
+
await fs.symlink(relativeTarget, linkPath, "dir");
|
|
1123
|
+
debug(`Symlinked ${agentDir}/${skillName} -> ${relativeTarget}`);
|
|
1124
|
+
} catch (err) {
|
|
1125
|
+
debug(`Symlink failed for ${agentDir}, falling back to copy`, err);
|
|
1126
|
+
try {
|
|
1127
|
+
await fs.cp(canonicalDir, linkPath, { recursive: true });
|
|
1128
|
+
} catch {
|
|
1129
|
+
debug(`Copy fallback also failed for ${agentDir}`);
|
|
1130
|
+
}
|
|
1131
|
+
}
|
|
1132
|
+
}
|
|
1133
|
+
}
|
|
1134
|
+
/**
|
|
1135
|
+
* Find the skills source directory shipped with the package.
|
|
1136
|
+
* Walks up from the current file to find the package root, then resolves skills/.
|
|
1137
|
+
*/
|
|
1138
|
+
async function findSkillsSourceDir() {
|
|
1139
|
+
let dir = path.dirname(fileURLToPath(import.meta.url));
|
|
1140
|
+
for (let i = 0; i < 10; i++) try {
|
|
1141
|
+
await fs.access(path.join(dir, "package.json"));
|
|
1142
|
+
const candidate = path.join(dir, "skills");
|
|
1143
|
+
await fs.access(path.join(candidate, "SKILL.md"));
|
|
1144
|
+
return candidate;
|
|
1145
|
+
} catch {
|
|
1146
|
+
const parent = path.dirname(dir);
|
|
1147
|
+
if (parent === dir) break;
|
|
1148
|
+
dir = parent;
|
|
1149
|
+
}
|
|
1150
|
+
return null;
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1045
1153
|
//#endregion
|
|
1046
1154
|
//#region src/helpers/installer.ts
|
|
1047
1155
|
/**
|
|
@@ -1153,7 +1261,8 @@ var Installer = class {
|
|
|
1153
1261
|
this.ensureTsConfig(),
|
|
1154
1262
|
this.ensurePrettierConfig(),
|
|
1155
1263
|
this.ensureFramerDeclarations(),
|
|
1156
|
-
this.ensurePackageJson()
|
|
1264
|
+
this.ensurePackageJson(),
|
|
1265
|
+
this.ensureSkills()
|
|
1157
1266
|
]);
|
|
1158
1267
|
Promise.resolve().then(async () => {
|
|
1159
1268
|
await this.ensureReact18Types();
|
|
@@ -1304,6 +1413,9 @@ declare module "*.json"
|
|
|
1304
1413
|
debug("Created package.json");
|
|
1305
1414
|
}
|
|
1306
1415
|
}
|
|
1416
|
+
async ensureSkills() {
|
|
1417
|
+
await installSkills(this.projectDir);
|
|
1418
|
+
}
|
|
1307
1419
|
async ensureReact18Types() {
|
|
1308
1420
|
const reactTypesDir = path.join(this.projectDir, "node_modules/@types/react");
|
|
1309
1421
|
const reactFiles = [
|
|
@@ -1781,11 +1893,17 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
|
|
|
1781
1893
|
if (explicitDir) {
|
|
1782
1894
|
const resolved = path.resolve(explicitDir);
|
|
1783
1895
|
await fs.mkdir(path.join(resolved, "files"), { recursive: true });
|
|
1784
|
-
return
|
|
1896
|
+
return {
|
|
1897
|
+
dir: resolved,
|
|
1898
|
+
created: false
|
|
1899
|
+
};
|
|
1785
1900
|
}
|
|
1786
1901
|
const cwd = process.cwd();
|
|
1787
1902
|
const existing = await findExistingProjectDir(cwd, projectHash);
|
|
1788
|
-
if (existing) return
|
|
1903
|
+
if (existing) return {
|
|
1904
|
+
dir: existing,
|
|
1905
|
+
created: false
|
|
1906
|
+
};
|
|
1789
1907
|
if (!projectName) throw new Error("Failed to get Project name. Pass --name <project name>.");
|
|
1790
1908
|
const dirName = toDirName(projectName);
|
|
1791
1909
|
const pkgName = toPackageName(projectName);
|
|
@@ -1797,11 +1915,13 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
|
|
|
1797
1915
|
version: "1.0.0",
|
|
1798
1916
|
private: true,
|
|
1799
1917
|
shortProjectHash: shortId,
|
|
1800
|
-
framerProjectHash: projectHash,
|
|
1801
1918
|
framerProjectName: projectName
|
|
1802
1919
|
};
|
|
1803
1920
|
await fs.writeFile(path.join(projectDir, "package.json"), JSON.stringify(pkg, null, 2));
|
|
1804
|
-
return
|
|
1921
|
+
return {
|
|
1922
|
+
dir: projectDir,
|
|
1923
|
+
created: true
|
|
1924
|
+
};
|
|
1805
1925
|
}
|
|
1806
1926
|
async function findExistingProjectDir(baseDir, projectHash) {
|
|
1807
1927
|
if (await matchesProject(path.join(baseDir, "package.json"), projectHash)) return baseDir;
|
|
@@ -2231,7 +2351,9 @@ async function executeEffect(effect, context) {
|
|
|
2231
2351
|
case "INIT_WORKSPACE":
|
|
2232
2352
|
if (!config.projectDir) {
|
|
2233
2353
|
const projectName = config.explicitName ?? effect.projectInfo.projectName;
|
|
2234
|
-
|
|
2354
|
+
const result = await findOrCreateProjectDir(config.projectHash, projectName, config.explicitDir);
|
|
2355
|
+
config.projectDir = result.dir;
|
|
2356
|
+
config.projectDirCreated = result.created;
|
|
2235
2357
|
config.filesDir = `${config.projectDir}/files`;
|
|
2236
2358
|
debug(`Files directory: ${config.filesDir}`);
|
|
2237
2359
|
await fs.mkdir(config.filesDir, { recursive: true });
|
|
@@ -2383,7 +2505,12 @@ async function executeEffect(effect, context) {
|
|
|
2383
2505
|
resetDisconnectState();
|
|
2384
2506
|
return [];
|
|
2385
2507
|
}
|
|
2386
|
-
|
|
2508
|
+
const relativeDir = config.projectDir ? "./" + (path.relative(process.cwd(), config.projectDir) || ".") : null;
|
|
2509
|
+
if (effect.totalCount === 0 && relativeDir) if (config.projectDirCreated) success(`Created ${relativeDir} folder`);
|
|
2510
|
+
else success(`Syncing to ${relativeDir} folder`);
|
|
2511
|
+
else if (relativeDir && config.projectDirCreated) success(`Synced into ${relativeDir} (${effect.updatedCount} files added)`);
|
|
2512
|
+
else if (relativeDir) success(`Synced into ${relativeDir} (${effect.updatedCount} files updated, ${effect.unchangedCount} unchanged)`);
|
|
2513
|
+
else success(`Synced ${effect.totalCount} files (${effect.updatedCount} updated, ${effect.unchangedCount} unchanged)`);
|
|
2387
2514
|
status("Watching for changes...");
|
|
2388
2515
|
return [];
|
|
2389
2516
|
}
|
|
@@ -2444,6 +2571,8 @@ async function start(config) {
|
|
|
2444
2571
|
return;
|
|
2445
2572
|
}
|
|
2446
2573
|
(async () => {
|
|
2574
|
+
cancelDisconnectMessage();
|
|
2575
|
+
if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
|
|
2447
2576
|
await processEvent({
|
|
2448
2577
|
type: "HANDSHAKE",
|
|
2449
2578
|
socket: client,
|
|
@@ -2460,8 +2589,6 @@ async function start(config) {
|
|
|
2460
2589
|
await installer.initialize();
|
|
2461
2590
|
startWatcher();
|
|
2462
2591
|
}
|
|
2463
|
-
cancelDisconnectMessage();
|
|
2464
|
-
if (!wasRecentlyDisconnected() && !didShowDisconnect()) success(`Connected to ${message.projectName}`);
|
|
2465
2592
|
})();
|
|
2466
2593
|
});
|
|
2467
2594
|
async function handleMessage(message) {
|
|
@@ -2607,7 +2734,7 @@ program.name("framer-code-link").description("Sync Framer code components to you
|
|
|
2607
2734
|
const detected = await getProjectHashFromCwd();
|
|
2608
2735
|
if (detected) projectHash = detected;
|
|
2609
2736
|
else {
|
|
2610
|
-
console.error("No
|
|
2737
|
+
console.error("No Project ID provided and no existing Code Link directory found.");
|
|
2611
2738
|
console.error("Copy the command from the Code Link Plugin to get started.");
|
|
2612
2739
|
process.exit(1);
|
|
2613
2740
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framer-code-link",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.0",
|
|
4
4
|
"description": "CLI tool for syncing Framer code components - controller-centric architecture",
|
|
5
5
|
"main": "dist/index.mjs",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"bin": "./dist/index.mjs",
|
|
8
8
|
"files": [
|
|
9
|
-
"dist"
|
|
9
|
+
"dist",
|
|
10
|
+
"skills"
|
|
10
11
|
],
|
|
11
12
|
"scripts": {
|
|
12
13
|
"dev": "NODE_ENV=development tsx src/index.ts",
|
package/skills/SKILL.md
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: framer-component-best-practices
|
|
3
|
+
description: Best practices for building and improving React code components in Framer, a no-code website builder. Covers property controls, animations, accessibility, and platform constraints. Use when creating, editing, or reviewing Framer components, working with ControlType property controls, or building React components for Framer projects.
|
|
4
|
+
license: MIT
|
|
5
|
+
metadata:
|
|
6
|
+
author: framer
|
|
7
|
+
version: "1.0"
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
# Framer Component Best Practices
|
|
11
|
+
|
|
12
|
+
Best practices for building and improving React components in Framer with property controls, animations, and accessibility.
|
|
13
|
+
|
|
14
|
+
## Core Rules
|
|
15
|
+
|
|
16
|
+
### Component Structure
|
|
17
|
+
|
|
18
|
+
```tsx
|
|
19
|
+
import { addPropertyControls, ControlType } from "framer";
|
|
20
|
+
import { motion } from "framer-motion"; // NOT from "framer"
|
|
21
|
+
|
|
22
|
+
interface MyComponentProps {
|
|
23
|
+
/* typed props */
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* @framerSupportedLayoutWidth any-prefer-fixed
|
|
28
|
+
* @framerSupportedLayoutHeight any-prefer-fixed
|
|
29
|
+
*/
|
|
30
|
+
export default function MyComponent(props: MyComponentProps) {
|
|
31
|
+
// component
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
addPropertyControls(MyComponent, {
|
|
35
|
+
/* controls */
|
|
36
|
+
});
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
### Platform Constraints
|
|
40
|
+
|
|
41
|
+
These will cause errors if violated:
|
|
42
|
+
|
|
43
|
+
1. **Single file, default export** - Use named `function` syntax (not arrow functions), no named exports
|
|
44
|
+
2. **Imports** - Only `react`, `react-dom`, `framer`, `framer-motion`. Import `motion` from `"framer-motion"`, not `"framer"`
|
|
45
|
+
3. **Position** - Use `position: relative` on the root element, never `fixed`
|
|
46
|
+
4. **SSR** - Guard `window`/`document` access: `if (typeof window !== "undefined")`
|
|
47
|
+
5. **Annotations** - Include `@framerSupportedLayoutWidth/Height` in a `/** */` block comment immediately above the component function
|
|
48
|
+
6. **Types** - Provide a typed props interface (e.g. `MyComponentProps`). Avoid NodeJS types like `Timeout` — use `number` instead
|
|
49
|
+
|
|
50
|
+
### Layout Annotations
|
|
51
|
+
|
|
52
|
+
| Content | Width | Height |
|
|
53
|
+
| ----------------- | ------------------ | ------------------ |
|
|
54
|
+
| No intrinsic size | `fixed` | `fixed` |
|
|
55
|
+
| Text/auto-sizing | `auto` | `auto` |
|
|
56
|
+
| Flexible | `any-prefer-fixed` | `any-prefer-fixed` |
|
|
57
|
+
|
|
58
|
+
Detect auto vs fixed sizing: check if `style.width` or `style.height` is `"100%"`.
|
|
59
|
+
|
|
60
|
+
### Property Controls
|
|
61
|
+
|
|
62
|
+
To make components configurable in Framer's properties panel, add property controls:
|
|
63
|
+
|
|
64
|
+
- To make colors customizable, use `ControlType.Color`. Reuse the same prop for elements sharing a color.
|
|
65
|
+
- To make text styling customizable, use `ControlType.Font` with `controls: "extended"` and `defaultFontType: "sans-serif"`.
|
|
66
|
+
- For images, use `ControlType.ResponsiveImage`. Set defaults in the component body via destructuring (the control doesn't support `defaultValue`).
|
|
67
|
+
- Provide a `defaultValue` for every prop so components render correctly in the Framer canvas. Include at least one item in `ControlType.Array` controls.
|
|
68
|
+
- `ComponentName.defaultProps` is not supported in Framer — use `defaultValue` on the property control instead.
|
|
69
|
+
- Use `hidden` for conditional visibility: `hidden: (props) => !props.showFeature`
|
|
70
|
+
- Prefer sliders over steppers unless step values are large.
|
|
71
|
+
- Keep controls focused — make key elements configurable, hardcode the rest.
|
|
72
|
+
- See [Property Control Guide](references/PROPERTY_CONTROL_GUIDE.md) for detailed patterns, font styling rules, and recommended default values.
|
|
73
|
+
|
|
74
|
+
### Image Defaults (in component body)
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
const {
|
|
78
|
+
image = {
|
|
79
|
+
src: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
|
|
80
|
+
alt: "Default",
|
|
81
|
+
},
|
|
82
|
+
} = props;
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### Animation Performance
|
|
86
|
+
|
|
87
|
+
```tsx
|
|
88
|
+
import { useIsStaticRenderer } from "framer";
|
|
89
|
+
import { useInView } from "framer-motion";
|
|
90
|
+
|
|
91
|
+
const isStatic = useIsStaticRenderer();
|
|
92
|
+
const ref = useRef(null);
|
|
93
|
+
const isInView = useInView(ref);
|
|
94
|
+
|
|
95
|
+
if (isStatic) return <StaticPreview />; // Show useful static state
|
|
96
|
+
// Pause animations when out of viewport
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- For very complex animations, consider WebGL instead of `framer-motion`.
|
|
100
|
+
- Static preview should include visual effects, not just text.
|
|
101
|
+
- Wrapping state updates in `startTransition()` prevents UI blocking and keeps interactions smooth.
|
|
102
|
+
|
|
103
|
+
### Text
|
|
104
|
+
|
|
105
|
+
- For auto-sized components with text, apply `width: max-content` or `minWidth: max-content` to prevent text from collapsing.
|
|
106
|
+
|
|
107
|
+
### Common Errors
|
|
108
|
+
|
|
109
|
+
- WebGL cross-origin: handle `SecurityError: Failed to execute 'texImage2D'` for cross-origin images.
|
|
110
|
+
- Inverted Y-axis: check if WebGL images render upside down and accommodate.
|
|
111
|
+
|
|
112
|
+
### Accessibility
|
|
113
|
+
|
|
114
|
+
- `aria` roles on interactive elements
|
|
115
|
+
- Semantic HTML (`<nav>`, `<article>`, `<section>`)
|
|
116
|
+
- `alt=""` on decorative images
|
|
117
|
+
- 4.5:1 color contrast
|
|
118
|
+
|
|
119
|
+
## Term Interpretation
|
|
120
|
+
|
|
121
|
+
- "responsive" → width/height 100%
|
|
122
|
+
- "modern" → 8px radius, 16px spacing, subtle shadows
|
|
123
|
+
- "minimal" → limited colors, whitespace
|
|
124
|
+
- "interactive" → hover/active states
|
|
125
|
+
- "accessible" → ARIA, semantic HTML
|
|
126
|
+
- "props"/"properties" → Framer property controls
|
|
127
|
+
|
|
128
|
+
## Reference Files
|
|
129
|
+
|
|
130
|
+
- [Property Controls](references/PROPERTY_CONTROLS.md) - All ControlType documentation with examples
|
|
131
|
+
- [Property Control Types](references/PROPERTY_CONTROL_TYPES.md) - TypeScript interfaces for all control types
|
|
132
|
+
- [Property Control Guide](references/PROPERTY_CONTROL_GUIDE.md) - Font patterns, styling rules, and recommended default values
|
|
133
|
+
- [Example Components](references/EXAMPLES.md) - Cookie banner, image compare, sticky notes, twemoji
|