framer-dalton 0.0.2 → 0.0.5
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 +16 -6
- package/dist/cli.js +14035 -6321
- package/dist/start-relay-server.js +158 -165
- package/docs/all-skills.md +6 -0
- package/docs/code-components.md +115 -0
- package/docs/component-examples.md +869 -0
- package/docs/property-controls.md +1535 -0
- package/docs/server-api.md +755 -0
- package/package.json +7 -3
|
@@ -3,13 +3,17 @@ import os from 'os';
|
|
|
3
3
|
import path from 'path';
|
|
4
4
|
import 'child_process';
|
|
5
5
|
import { fileURLToPath } from 'url';
|
|
6
|
+
import { createTRPCClient, httpLink } from '@trpc/client';
|
|
6
7
|
import http from 'http';
|
|
8
|
+
import { createHTTPHandler } from '@trpc/server/adapters/standalone';
|
|
9
|
+
import { initTRPC, TRPCError } from '@trpc/server';
|
|
10
|
+
import { z } from 'zod';
|
|
7
11
|
import crypto from 'crypto';
|
|
8
12
|
import { createRequire } from 'module';
|
|
9
13
|
import * as vm from 'vm';
|
|
10
14
|
import { connect } from 'framer-api';
|
|
11
15
|
|
|
12
|
-
/* @framer/ai relay server v0.0.
|
|
16
|
+
/* @framer/ai relay server v0.0.5 */
|
|
13
17
|
var __defProp = Object.defineProperty;
|
|
14
18
|
var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
|
|
15
19
|
function getLogPath() {
|
|
@@ -46,8 +50,15 @@ function log(message) {
|
|
|
46
50
|
__name(log, "log");
|
|
47
51
|
var __filename$1 = fileURLToPath(import.meta.url);
|
|
48
52
|
path.dirname(__filename$1);
|
|
49
|
-
var VERSION = "0.0.
|
|
50
|
-
var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) ||
|
|
53
|
+
var VERSION = "0.0.5" ;
|
|
54
|
+
var RELAY_PORT = Number(process.env.FRAMER_CLI_PORT) || 19988;
|
|
55
|
+
createTRPCClient({
|
|
56
|
+
links: [
|
|
57
|
+
httpLink({
|
|
58
|
+
url: `http://127.0.0.1:${RELAY_PORT}`
|
|
59
|
+
})
|
|
60
|
+
]
|
|
61
|
+
});
|
|
51
62
|
|
|
52
63
|
// src/connection-errors.ts
|
|
53
64
|
var CONNECTION_ERROR_PATTERNS = [
|
|
@@ -92,14 +103,14 @@ var ScopedFS = class {
|
|
|
92
103
|
resolvePath(filePath) {
|
|
93
104
|
const resolved = path.resolve(filePath);
|
|
94
105
|
if (!this.isPathAllowed(resolved)) {
|
|
95
|
-
const
|
|
106
|
+
const error = new Error(
|
|
96
107
|
`EPERM: operation not permitted, access outside allowed directories: ${filePath}`
|
|
97
108
|
);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
throw
|
|
109
|
+
error.code = "EPERM";
|
|
110
|
+
error.errno = -1;
|
|
111
|
+
error.syscall = "access";
|
|
112
|
+
error.path = filePath;
|
|
113
|
+
throw error;
|
|
103
114
|
}
|
|
104
115
|
return resolved;
|
|
105
116
|
}
|
|
@@ -255,8 +266,8 @@ var ScopedFS = class {
|
|
|
255
266
|
constants = fs2.constants;
|
|
256
267
|
};
|
|
257
268
|
|
|
258
|
-
// src/
|
|
259
|
-
var
|
|
269
|
+
// src/execute.ts
|
|
270
|
+
var EXECUTION_TIMEOUT = 10 * 60 * 1e3;
|
|
260
271
|
var baseRequire = createRequire(import.meta.url);
|
|
261
272
|
var ALLOWED_MODULES = /* @__PURE__ */ new Set([
|
|
262
273
|
"path",
|
|
@@ -279,11 +290,11 @@ var ALLOWED_MODULES = /* @__PURE__ */ new Set([
|
|
|
279
290
|
function createSandboxedRequire(scopedFs) {
|
|
280
291
|
const sandboxedRequire = /* @__PURE__ */ __name(((id) => {
|
|
281
292
|
if (!ALLOWED_MODULES.has(id)) {
|
|
282
|
-
const
|
|
293
|
+
const error = new Error(
|
|
283
294
|
`Module "${id}" is not allowed. Allowed: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith("node:")).join(", ")}`
|
|
284
295
|
);
|
|
285
|
-
|
|
286
|
-
throw
|
|
296
|
+
error.name = "ModuleNotAllowedError";
|
|
297
|
+
throw error;
|
|
287
298
|
}
|
|
288
299
|
if (id === "fs" || id === "node:fs") {
|
|
289
300
|
return scopedFs;
|
|
@@ -302,11 +313,11 @@ function createSandboxedRequire(scopedFs) {
|
|
|
302
313
|
__name(createSandboxedRequire, "createSandboxedRequire");
|
|
303
314
|
async function sandboxedImport(scopedFs, specifier) {
|
|
304
315
|
if (!ALLOWED_MODULES.has(specifier)) {
|
|
305
|
-
const
|
|
316
|
+
const error = new Error(
|
|
306
317
|
`Module "${specifier}" is not allowed. Allowed: ${[...ALLOWED_MODULES].filter((m) => !m.startsWith("node:")).join(", ")}`
|
|
307
318
|
);
|
|
308
|
-
|
|
309
|
-
throw
|
|
319
|
+
error.name = "ModuleNotAllowedError";
|
|
320
|
+
throw error;
|
|
310
321
|
}
|
|
311
322
|
if (specifier === "fs" || specifier === "node:fs") {
|
|
312
323
|
return scopedFs;
|
|
@@ -317,8 +328,8 @@ async function sandboxedImport(scopedFs, specifier) {
|
|
|
317
328
|
return import(specifier);
|
|
318
329
|
}
|
|
319
330
|
__name(sandboxedImport, "sandboxedImport");
|
|
320
|
-
async function execute(session,
|
|
321
|
-
const {
|
|
331
|
+
async function execute(session, framer, code, options = {}) {
|
|
332
|
+
const { cwd } = options;
|
|
322
333
|
const output = [];
|
|
323
334
|
const customConsole = {
|
|
324
335
|
log: /* @__PURE__ */ __name((...args) => {
|
|
@@ -334,11 +345,11 @@ async function execute(session, connection, code, options = {}) {
|
|
|
334
345
|
output.push(args.map((arg) => formatValue(arg)).join(" "));
|
|
335
346
|
}, "info")
|
|
336
347
|
};
|
|
337
|
-
const scopedFs = cwd ? new ScopedFS([cwd, "/tmp"]) : new ScopedFS();
|
|
348
|
+
const scopedFs = cwd ? new ScopedFS([cwd, "/tmp", os.tmpdir()]) : new ScopedFS();
|
|
338
349
|
const sandboxedRequire = createSandboxedRequire(scopedFs);
|
|
339
350
|
const vmContextObj = {
|
|
340
351
|
// Framer API
|
|
341
|
-
framer
|
|
352
|
+
framer,
|
|
342
353
|
state: session.state,
|
|
343
354
|
// Console
|
|
344
355
|
console: customConsole,
|
|
@@ -365,22 +376,24 @@ async function execute(session, connection, code, options = {}) {
|
|
|
365
376
|
};
|
|
366
377
|
const vmContext = vm.createContext(vmContextObj);
|
|
367
378
|
const wrappedCode = `(async () => { ${code} })()`;
|
|
379
|
+
let timeoutId;
|
|
368
380
|
try {
|
|
369
381
|
const script = new vm.Script(wrappedCode, {
|
|
370
382
|
filename: "framer-exec.js"
|
|
371
383
|
});
|
|
372
384
|
const resultPromise = script.runInContext(vmContext, {
|
|
373
385
|
timeout: 5e3
|
|
374
|
-
// Short timeout for sync part
|
|
375
386
|
});
|
|
376
387
|
const result = await Promise.race([
|
|
377
388
|
resultPromise,
|
|
378
|
-
new Promise(
|
|
379
|
-
|
|
380
|
-
() => reject(
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
389
|
+
new Promise((_, reject) => {
|
|
390
|
+
timeoutId = setTimeout(
|
|
391
|
+
() => reject(
|
|
392
|
+
new Error(`Execution timed out after ${EXECUTION_TIMEOUT}ms`)
|
|
393
|
+
),
|
|
394
|
+
EXECUTION_TIMEOUT
|
|
395
|
+
);
|
|
396
|
+
})
|
|
384
397
|
]);
|
|
385
398
|
if (result !== void 0) {
|
|
386
399
|
output.push(formatValue(result));
|
|
@@ -392,9 +405,42 @@ async function execute(session, connection, code, options = {}) {
|
|
|
392
405
|
output,
|
|
393
406
|
error: errorMessage
|
|
394
407
|
};
|
|
408
|
+
} finally {
|
|
409
|
+
if (timeoutId) clearTimeout(timeoutId);
|
|
395
410
|
}
|
|
396
411
|
}
|
|
397
412
|
__name(execute, "execute");
|
|
413
|
+
async function executeWithReconnect(session, framer, code, options, reconnect) {
|
|
414
|
+
const result = await tryExecute(session, framer, code, options);
|
|
415
|
+
if (!result.error || !isConnectionError(result.error)) {
|
|
416
|
+
return result;
|
|
417
|
+
}
|
|
418
|
+
log(
|
|
419
|
+
`reconnect session=${session.id} project=${session.projectId} reason="${result.error}"`
|
|
420
|
+
);
|
|
421
|
+
const newFramer = await reconnect();
|
|
422
|
+
if (!newFramer) {
|
|
423
|
+
log(
|
|
424
|
+
`reconnect.failed session=${session.id} error="no connection returned"`
|
|
425
|
+
);
|
|
426
|
+
return {
|
|
427
|
+
output: [],
|
|
428
|
+
error: "Connection lost and failed to reconnect"
|
|
429
|
+
};
|
|
430
|
+
}
|
|
431
|
+
log(`reconnect.success session=${session.id}`);
|
|
432
|
+
return tryExecute(session, newFramer, code, options);
|
|
433
|
+
}
|
|
434
|
+
__name(executeWithReconnect, "executeWithReconnect");
|
|
435
|
+
async function tryExecute(session, framer, code, options) {
|
|
436
|
+
try {
|
|
437
|
+
return await execute(session, framer, code, options);
|
|
438
|
+
} catch (err) {
|
|
439
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
440
|
+
return { output: [], error };
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
__name(tryExecute, "tryExecute");
|
|
398
444
|
function formatValue(value) {
|
|
399
445
|
if (value === null) return "null";
|
|
400
446
|
if (value === void 0) return "undefined";
|
|
@@ -420,6 +466,7 @@ var ConnectionPool = class {
|
|
|
420
466
|
__name(this, "ConnectionPool");
|
|
421
467
|
}
|
|
422
468
|
pool = /* @__PURE__ */ new Map();
|
|
469
|
+
reconnectPromises = /* @__PURE__ */ new Map();
|
|
423
470
|
/**
|
|
424
471
|
* Acquire a connection for a session.
|
|
425
472
|
* If a connection already exists for the project, the session is added to it.
|
|
@@ -447,15 +494,26 @@ var ConnectionPool = class {
|
|
|
447
494
|
}
|
|
448
495
|
/**
|
|
449
496
|
* Reconnect a project's connection (call after catching a connection error).
|
|
450
|
-
*
|
|
497
|
+
* Concurrent callers for the same project share a single reconnect attempt.
|
|
451
498
|
*/
|
|
452
499
|
async reconnect(projectId) {
|
|
500
|
+
const existingPromise = this.reconnectPromises.get(projectId);
|
|
501
|
+
if (existingPromise) return existingPromise;
|
|
502
|
+
const promise = this.doReconnect(projectId).finally(() => {
|
|
503
|
+
this.reconnectPromises.delete(projectId);
|
|
504
|
+
});
|
|
505
|
+
this.reconnectPromises.set(projectId, promise);
|
|
506
|
+
return promise;
|
|
507
|
+
}
|
|
508
|
+
async doReconnect(projectId) {
|
|
453
509
|
const entry = this.pool.get(projectId);
|
|
454
|
-
if (!entry)
|
|
510
|
+
if (!entry) return null;
|
|
511
|
+
try {
|
|
512
|
+
await entry.connection.reconnect();
|
|
513
|
+
return entry.connection;
|
|
514
|
+
} catch {
|
|
455
515
|
return null;
|
|
456
516
|
}
|
|
457
|
-
await entry.connection.reconnect();
|
|
458
|
-
return entry.connection;
|
|
459
517
|
}
|
|
460
518
|
/**
|
|
461
519
|
* Release a session from a connection.
|
|
@@ -515,7 +573,7 @@ var SessionManager = class {
|
|
|
515
573
|
get(id) {
|
|
516
574
|
return this.sessions.get(id);
|
|
517
575
|
}
|
|
518
|
-
|
|
576
|
+
getFramer(session) {
|
|
519
577
|
return connectionPool.getConnection(session.projectId);
|
|
520
578
|
}
|
|
521
579
|
async reconnect(session) {
|
|
@@ -537,144 +595,79 @@ var SessionManager = class {
|
|
|
537
595
|
};
|
|
538
596
|
var sessionManager = new SessionManager();
|
|
539
597
|
|
|
598
|
+
// src/router.ts
|
|
599
|
+
var t = initTRPC.create();
|
|
600
|
+
var appRouter = t.router({
|
|
601
|
+
version: t.procedure.query(() => {
|
|
602
|
+
return { version: VERSION };
|
|
603
|
+
}),
|
|
604
|
+
listSessions: t.procedure.query(() => {
|
|
605
|
+
return sessionManager.list();
|
|
606
|
+
}),
|
|
607
|
+
createSession: t.procedure.input(z.object({ projectId: z.string(), apiKey: z.string() })).mutation(async ({ input }) => {
|
|
608
|
+
const id = await sessionManager.create(input.projectId, input.apiKey);
|
|
609
|
+
log(`session.new id=${id} project=${input.projectId}`);
|
|
610
|
+
return { id };
|
|
611
|
+
}),
|
|
612
|
+
destroySession: t.procedure.input(z.object({ sessionId: z.string() })).mutation(async ({ input }) => {
|
|
613
|
+
await sessionManager.destroy(input.sessionId);
|
|
614
|
+
log(`session.destroy id=${input.sessionId}`);
|
|
615
|
+
}),
|
|
616
|
+
exec: t.procedure.input(
|
|
617
|
+
z.object({
|
|
618
|
+
sessionId: z.string(),
|
|
619
|
+
code: z.string(),
|
|
620
|
+
cwd: z.string().optional()
|
|
621
|
+
})
|
|
622
|
+
).mutation(async ({ input }) => {
|
|
623
|
+
const { sessionId, code, cwd } = input;
|
|
624
|
+
const session = sessionManager.get(sessionId);
|
|
625
|
+
if (!session) {
|
|
626
|
+
throw new TRPCError({
|
|
627
|
+
code: "NOT_FOUND",
|
|
628
|
+
message: `Session ${sessionId} not found`
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
log(
|
|
632
|
+
`exec session=${sessionId} code=${JSON.stringify(code).slice(0, 100)}`
|
|
633
|
+
);
|
|
634
|
+
const framer = sessionManager.getFramer(session);
|
|
635
|
+
if (!framer) {
|
|
636
|
+
return {
|
|
637
|
+
output: [],
|
|
638
|
+
error: "Failed to get connection for session"
|
|
639
|
+
};
|
|
640
|
+
}
|
|
641
|
+
const result = await executeWithReconnect(
|
|
642
|
+
session,
|
|
643
|
+
framer,
|
|
644
|
+
code,
|
|
645
|
+
{ cwd },
|
|
646
|
+
() => sessionManager.reconnect(session)
|
|
647
|
+
);
|
|
648
|
+
if (result.error) {
|
|
649
|
+
log(`exec.error session=${sessionId} error="${result.error}"`);
|
|
650
|
+
}
|
|
651
|
+
return result;
|
|
652
|
+
}),
|
|
653
|
+
shutdown: t.procedure.mutation(() => {
|
|
654
|
+
log("shutdown requested");
|
|
655
|
+
setTimeout(() => {
|
|
656
|
+
process.exit(0);
|
|
657
|
+
}, 100);
|
|
658
|
+
})
|
|
659
|
+
});
|
|
660
|
+
|
|
540
661
|
// src/relay-server.ts
|
|
541
|
-
|
|
542
|
-
res.writeHead(status, { "Content-Type": "application/json" });
|
|
543
|
-
res.end(JSON.stringify(data));
|
|
544
|
-
}
|
|
545
|
-
__name(json, "json");
|
|
546
|
-
function error(res, message, status = 400) {
|
|
547
|
-
json(res, { error: message }, status);
|
|
548
|
-
}
|
|
549
|
-
__name(error, "error");
|
|
550
|
-
async function readBody(req) {
|
|
551
|
-
return new Promise((resolve, reject) => {
|
|
552
|
-
let body = "";
|
|
553
|
-
req.on("data", (chunk) => {
|
|
554
|
-
body += chunk;
|
|
555
|
-
});
|
|
556
|
-
req.on("end", () => {
|
|
557
|
-
try {
|
|
558
|
-
resolve(body ? JSON.parse(body) : {});
|
|
559
|
-
} catch {
|
|
560
|
-
reject(new Error("Invalid JSON body"));
|
|
561
|
-
}
|
|
562
|
-
});
|
|
563
|
-
req.on("error", reject);
|
|
564
|
-
});
|
|
565
|
-
}
|
|
566
|
-
__name(readBody, "readBody");
|
|
662
|
+
var trpcHandler = createHTTPHandler({ router: appRouter });
|
|
567
663
|
async function startRelayServer(port = RELAY_PORT) {
|
|
568
|
-
const server = http.createServer(
|
|
569
|
-
|
|
570
|
-
try {
|
|
571
|
-
if (req.method === "GET" && url === "/version") {
|
|
572
|
-
return json(res, { version: VERSION });
|
|
573
|
-
}
|
|
574
|
-
if (req.method === "GET" && url === "/cli/sessions") {
|
|
575
|
-
const sessions = sessionManager.list();
|
|
576
|
-
return json(res, { sessions });
|
|
577
|
-
}
|
|
578
|
-
if (req.method === "POST" && url === "/cli/session/new") {
|
|
579
|
-
const body = await readBody(req);
|
|
580
|
-
const projectId = body.projectId;
|
|
581
|
-
const apiKey = body.apiKey;
|
|
582
|
-
if (!projectId || !apiKey) {
|
|
583
|
-
return error(res, "projectId and apiKey are required");
|
|
584
|
-
}
|
|
585
|
-
const id = await sessionManager.create(projectId, apiKey);
|
|
586
|
-
log(`session.new id=${id} project=${projectId}`);
|
|
587
|
-
return json(res, { id });
|
|
588
|
-
}
|
|
589
|
-
if (req.method === "POST" && url === "/cli/session/destroy") {
|
|
590
|
-
const body = await readBody(req);
|
|
591
|
-
const sessionId = body.sessionId ? String(body.sessionId) : "";
|
|
592
|
-
if (!sessionId) {
|
|
593
|
-
return error(res, "sessionId is required");
|
|
594
|
-
}
|
|
595
|
-
await sessionManager.destroy(sessionId);
|
|
596
|
-
log(`session.destroy id=${sessionId}`);
|
|
597
|
-
return json(res, { success: true });
|
|
598
|
-
}
|
|
599
|
-
if (req.method === "POST" && url === "/cli/exec") {
|
|
600
|
-
const body = await readBody(req);
|
|
601
|
-
const sessionId = String(body.sessionId);
|
|
602
|
-
const code = body.code;
|
|
603
|
-
const timeout = body.timeout || 3e4;
|
|
604
|
-
const cwd = body.cwd;
|
|
605
|
-
if (!sessionId) {
|
|
606
|
-
return error(res, "sessionId is required");
|
|
607
|
-
}
|
|
608
|
-
if (!code) {
|
|
609
|
-
return error(res, "code is required");
|
|
610
|
-
}
|
|
611
|
-
const session = sessionManager.get(sessionId);
|
|
612
|
-
if (!session) {
|
|
613
|
-
return error(res, `Session ${sessionId} not found`, 404);
|
|
614
|
-
}
|
|
615
|
-
log(
|
|
616
|
-
`exec session=${sessionId} code=${JSON.stringify(code).slice(0, 100)}`
|
|
617
|
-
);
|
|
618
|
-
const connection = sessionManager.getConnection(session);
|
|
619
|
-
if (!connection) {
|
|
620
|
-
return json(res, {
|
|
621
|
-
output: [],
|
|
622
|
-
error: "Failed to get connection for session"
|
|
623
|
-
});
|
|
624
|
-
}
|
|
625
|
-
let result;
|
|
626
|
-
try {
|
|
627
|
-
result = await execute(session, connection, code, { timeout, cwd });
|
|
628
|
-
} catch (execErr) {
|
|
629
|
-
const errMsg = execErr instanceof Error ? execErr.message : String(execErr);
|
|
630
|
-
result = { output: [], error: errMsg };
|
|
631
|
-
}
|
|
632
|
-
if (result.error && isConnectionError(result.error)) {
|
|
633
|
-
log(
|
|
634
|
-
`reconnect session=${sessionId} project=${session.projectId} reason="${result.error}"`
|
|
635
|
-
);
|
|
636
|
-
const newConnection = await sessionManager.reconnect(session);
|
|
637
|
-
if (newConnection) {
|
|
638
|
-
try {
|
|
639
|
-
result = await execute(session, newConnection, code, {
|
|
640
|
-
timeout,
|
|
641
|
-
cwd
|
|
642
|
-
});
|
|
643
|
-
log(`reconnect.success session=${sessionId}`);
|
|
644
|
-
} catch (retryErr) {
|
|
645
|
-
const errMsg = retryErr instanceof Error ? retryErr.message : String(retryErr);
|
|
646
|
-
log(`reconnect.failed session=${sessionId} error="${errMsg}"`);
|
|
647
|
-
result = { output: [], error: errMsg };
|
|
648
|
-
}
|
|
649
|
-
} else {
|
|
650
|
-
log(
|
|
651
|
-
`reconnect.failed session=${sessionId} error="no connection returned"`
|
|
652
|
-
);
|
|
653
|
-
}
|
|
654
|
-
}
|
|
655
|
-
if (result.error) {
|
|
656
|
-
log(`exec.error session=${sessionId} error="${result.error}"`);
|
|
657
|
-
}
|
|
658
|
-
return json(res, result);
|
|
659
|
-
}
|
|
660
|
-
if (req.method === "POST" && url === "/shutdown") {
|
|
661
|
-
log("shutdown requested");
|
|
662
|
-
json(res, { success: true });
|
|
663
|
-
setTimeout(() => {
|
|
664
|
-
process.exit(0);
|
|
665
|
-
}, 100);
|
|
666
|
-
return;
|
|
667
|
-
}
|
|
668
|
-
error(res, "Not found", 404);
|
|
669
|
-
} catch (err) {
|
|
670
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
671
|
-
log(`error: ${message}`);
|
|
672
|
-
error(res, message, 500);
|
|
673
|
-
}
|
|
664
|
+
const server = http.createServer((req, res) => {
|
|
665
|
+
trpcHandler(req, res);
|
|
674
666
|
});
|
|
675
667
|
return new Promise((resolve, reject) => {
|
|
676
668
|
server.on("error", reject);
|
|
677
669
|
server.listen(port, "127.0.0.1", () => {
|
|
670
|
+
log(`started v${VERSION} on port ${port}`);
|
|
678
671
|
resolve(server);
|
|
679
672
|
});
|
|
680
673
|
});
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
## All Skills
|
|
2
|
+
|
|
3
|
+
- `npx framer-dalton skill` — Server API docs (sessions, collections, canvas, styles, publishing)
|
|
4
|
+
- `npx framer-dalton skill code-components` — Code component authoring guide
|
|
5
|
+
- `npx framer-dalton skill property-controls` — Property control reference (all 20+ types)
|
|
6
|
+
- `npx framer-dalton skill component-examples` — Complete code component examples
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Framer Code Components
|
|
2
|
+
|
|
3
|
+
## Best Practices
|
|
4
|
+
|
|
5
|
+
### Component Structure
|
|
6
|
+
|
|
7
|
+
```tsx
|
|
8
|
+
import { addPropertyControls, ControlType } from "framer";
|
|
9
|
+
import { motion } from "framer-motion"; // NOT from "framer"
|
|
10
|
+
|
|
11
|
+
interface MyComponentProps {
|
|
12
|
+
/* typed props */
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* @framerSupportedLayoutWidth any-prefer-fixed
|
|
17
|
+
* @framerSupportedLayoutHeight any-prefer-fixed
|
|
18
|
+
*/
|
|
19
|
+
export default function MyComponent(props: MyComponentProps) {
|
|
20
|
+
// component
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
addPropertyControls(MyComponent, {
|
|
24
|
+
/* controls */
|
|
25
|
+
});
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
### Platform Constraints
|
|
29
|
+
|
|
30
|
+
These will cause errors if violated:
|
|
31
|
+
|
|
32
|
+
1. **Single file, default export** - Use named `function` syntax (not arrow functions), no named exports
|
|
33
|
+
2. **Imports** - Only `react`, `react-dom`, `framer`, `framer-motion`. Import `motion` from `"framer-motion"`, not `"framer"`
|
|
34
|
+
3. **Position** - Use `position: relative` on the root element, never `fixed`
|
|
35
|
+
4. **SSR** - Guard `window`/`document` access: `if (typeof window !== "undefined")`
|
|
36
|
+
5. **Annotations** - Include `@framerSupportedLayoutWidth/Height` in a `/** */` block comment immediately above the component function
|
|
37
|
+
6. **Types** - Provide a typed props interface (e.g. `MyComponentProps`). Avoid NodeJS types like `Timeout` — use `number` instead
|
|
38
|
+
|
|
39
|
+
### Layout Annotations
|
|
40
|
+
|
|
41
|
+
| Content | Width | Height |
|
|
42
|
+
| ----------------- | ------------------ | ------------------ |
|
|
43
|
+
| No intrinsic size | `fixed` | `fixed` |
|
|
44
|
+
| Text/auto-sizing | `auto` | `auto` |
|
|
45
|
+
| Flexible | `any-prefer-fixed` | `any-prefer-fixed` |
|
|
46
|
+
|
|
47
|
+
Detect auto vs fixed sizing: check if `style.width` or `style.height` is `"100%"`.
|
|
48
|
+
|
|
49
|
+
### Property Controls
|
|
50
|
+
|
|
51
|
+
To make components configurable in Framer's properties panel, add property controls:
|
|
52
|
+
|
|
53
|
+
- To make colors customizable, use `ControlType.Color`. Reuse the same prop for elements sharing a color.
|
|
54
|
+
- To make text styling customizable, use `ControlType.Font` with `controls: "extended"` and `defaultFontType: "sans-serif"`.
|
|
55
|
+
- For images, use `ControlType.ResponsiveImage`. Set defaults in the component body via destructuring (the control doesn't support `defaultValue`).
|
|
56
|
+
- Provide a `defaultValue` for every prop so components render correctly in the Framer canvas. Include at least one item in `ControlType.Array` controls.
|
|
57
|
+
- `ComponentName.defaultProps` is not supported in Framer — use `defaultValue` on the property control instead.
|
|
58
|
+
- Use `hidden` for conditional visibility: `hidden: (props) => !props.showFeature`
|
|
59
|
+
- Prefer sliders over steppers unless step values are large.
|
|
60
|
+
- Keep controls focused — make key elements configurable, hardcode the rest.
|
|
61
|
+
- See `npx framer-dalton skill property-controls` for the full property control reference.
|
|
62
|
+
|
|
63
|
+
### Image Defaults (in component body)
|
|
64
|
+
|
|
65
|
+
```tsx
|
|
66
|
+
const {
|
|
67
|
+
image = {
|
|
68
|
+
src: "https://framerusercontent.com/images/GfGkADagM4KEibNcIiRUWlfrR0.jpg",
|
|
69
|
+
alt: "Default",
|
|
70
|
+
},
|
|
71
|
+
} = props;
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
### Animation Performance
|
|
75
|
+
|
|
76
|
+
```tsx
|
|
77
|
+
import { useIsStaticRenderer } from "framer";
|
|
78
|
+
import { useInView } from "framer-motion";
|
|
79
|
+
|
|
80
|
+
const isStatic = useIsStaticRenderer();
|
|
81
|
+
const ref = useRef(null);
|
|
82
|
+
const isInView = useInView(ref);
|
|
83
|
+
|
|
84
|
+
if (isStatic) return <StaticPreview />; // Show useful static state
|
|
85
|
+
// Pause animations when out of viewport
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
- For very complex animations, consider WebGL instead of `framer-motion`.
|
|
89
|
+
- Static preview should include visual effects, not just text.
|
|
90
|
+
- Wrapping state updates in `startTransition()` prevents UI blocking and keeps interactions smooth.
|
|
91
|
+
|
|
92
|
+
### Text
|
|
93
|
+
|
|
94
|
+
- For auto-sized components with text, apply `width: max-content` or `minWidth: max-content` to prevent text from collapsing.
|
|
95
|
+
|
|
96
|
+
### Common Errors
|
|
97
|
+
|
|
98
|
+
- WebGL cross-origin: handle `SecurityError: Failed to execute 'texImage2D'` for cross-origin images.
|
|
99
|
+
- Inverted Y-axis: check if WebGL images render upside down and accommodate.
|
|
100
|
+
|
|
101
|
+
### Accessibility
|
|
102
|
+
|
|
103
|
+
- `aria` roles on interactive elements
|
|
104
|
+
- Semantic HTML (`<nav>`, `<article>`, `<section>`)
|
|
105
|
+
- `alt=""` on decorative images
|
|
106
|
+
- 4.5:1 color contrast
|
|
107
|
+
|
|
108
|
+
## Term Interpretation
|
|
109
|
+
|
|
110
|
+
- "responsive" → width/height 100%
|
|
111
|
+
- "modern" → 8px radius, 16px spacing, subtle shadows
|
|
112
|
+
- "minimal" → limited colors, whitespace
|
|
113
|
+
- "interactive" → hover/active states
|
|
114
|
+
- "accessible" → ARIA, semantic HTML
|
|
115
|
+
- "props"/"properties" → Framer property controls
|