framer-code-link 0.6.0 → 0.8.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 +119 -13
- 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
|
@@ -7,9 +7,10 @@ import path from "path";
|
|
|
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 = [
|
|
@@ -1797,7 +1909,6 @@ async function findOrCreateProjectDir(projectHash, projectName, explicitDir) {
|
|
|
1797
1909
|
version: "1.0.0",
|
|
1798
1910
|
private: true,
|
|
1799
1911
|
shortProjectHash: shortId,
|
|
1800
|
-
framerProjectHash: projectHash,
|
|
1801
1912
|
framerProjectName: projectName
|
|
1802
1913
|
};
|
|
1803
1914
|
await fs.writeFile(path.join(projectDir, "package.json"), JSON.stringify(pkg, null, 2));
|
|
@@ -1990,12 +2101,9 @@ function transition(state, event) {
|
|
|
1990
2101
|
case "REMOTE_FILE_CHANGE": {
|
|
1991
2102
|
const validation = validateIncomingChange(event.fileMeta, state.mode);
|
|
1992
2103
|
if (validation.action === "queue") {
|
|
1993
|
-
effects.push(log("debug", `
|
|
2104
|
+
effects.push(log("debug", `Ignoring file change during sync: ${event.file.name}`));
|
|
1994
2105
|
return {
|
|
1995
|
-
state
|
|
1996
|
-
...state,
|
|
1997
|
-
pendingRemoteChanges: [...state.pendingRemoteChanges, event.file]
|
|
1998
|
-
},
|
|
2106
|
+
state,
|
|
1999
2107
|
effects
|
|
2000
2108
|
};
|
|
2001
2109
|
}
|
|
@@ -2413,9 +2521,7 @@ async function start(config) {
|
|
|
2413
2521
|
let syncState = {
|
|
2414
2522
|
mode: "disconnected",
|
|
2415
2523
|
socket: null,
|
|
2416
|
-
pendingRemoteChanges: []
|
|
2417
|
-
pendingOperations: /* @__PURE__ */ new Map(),
|
|
2418
|
-
nextOperationId: 1
|
|
2524
|
+
pendingRemoteChanges: []
|
|
2419
2525
|
};
|
|
2420
2526
|
const userActions = new PluginUserPromptCoordinator();
|
|
2421
2527
|
async function processEvent(event) {
|
|
@@ -2518,7 +2624,7 @@ async function start(config) {
|
|
|
2518
2624
|
await processEvent({
|
|
2519
2625
|
type: "LOCAL_DELETE_REJECTED",
|
|
2520
2626
|
fileName: file.fileName,
|
|
2521
|
-
content: file.content
|
|
2627
|
+
content: file.content
|
|
2522
2628
|
});
|
|
2523
2629
|
}
|
|
2524
2630
|
return;
|
|
@@ -2612,7 +2718,7 @@ program.name("framer-code-link").description("Sync Framer code components to you
|
|
|
2612
2718
|
const detected = await getProjectHashFromCwd();
|
|
2613
2719
|
if (detected) projectHash = detected;
|
|
2614
2720
|
else {
|
|
2615
|
-
console.error("No
|
|
2721
|
+
console.error("No Project ID provided and no existing Code Link directory found.");
|
|
2616
2722
|
console.error("Copy the command from the Code Link Plugin to get started.");
|
|
2617
2723
|
process.exit(1);
|
|
2618
2724
|
}
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "framer-code-link",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.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
|