Lição 38 - Do design ao código
Variáveis e temas
O Pencil suporta um sistema completo de tokens de design com valores condicionais para vários temas. Veja Variables and Themes. As variáveis reduzem a codificação de valores fixos: o modelo não precisa emitir valores de cor específicos (menos erros de #RRGGBB) nem aprender a tabela de tokens do seu design system — ele só precisa dos nomes semânticos das variáveis, e o renderizador os resolve.
api.setAppState({
variables: {
'color.background': { type: 'color', value: '#FFFFFF' },
'text.title': { type: 'number', value: 72 },
},
});
api.updateNodes([
{
id: 'r1',
type: 'rect',
x: 0,
y: 0,
width: 100,
height: 100,
zIndex: 1,
fill: '$color.background',
stroke: '$color.background',
},
]);Quando o modelo projeta para o modo escuro, ele não precisa de duas paletas completas — apenas de referências como $color.bg, enquanto o theme do nó controla o valor resolvido.
"variables": {
"color.bg": {
"type": "color",
"value": [
{ "value": "#FFFFFF", "theme": { "mode": "light" } },
{ "value": "#000000", "theme": { "mode": "dark" } }
]
}
}O modelo também não precisa calcular valores em pixels nem ligar breakpoints responsivos. Ele declara a intenção de forma declarativa, e o motor de layout calcula a geometria. Veja Lição 33 - Motor de layout.
Tipos de variável
O Figma suporta quatro tipos de variável. Veja Guide to variables in Figma
- Cor (
#000000) - Número
- String, por exemplo
fontFamilyou o conteúdo do texto - Booleano
Painel de propriedades e seletor de variáveis
Com um nó selecionado, o painel de propriedades Spectrum pode vincular variáveis de design de AppState.variables a preenchimento / cor do traço / largura do traço / tamanho da fonte: escolha um nome no menu suspenso para gravar $token. Quando está vinculado, aparece um selo roxo, e você pode desvincular em uma ação (grava de volta o literal resolvido atual e registra no histórico). O seletor de cor e o controle deslizante de largura do traço sempre mostram valores resolvidos, então um $... cru nunca é usado como cor CSS inválida.
Exportar SVG
Existem várias estratégias de exportação; o padrão são literais resolvidos:
export type DesignVariablesSvgExportMode =
/** Resolve $ → literal */
| 'resolved'
/** Keep `$token` strings (attributes may be non-standard; good for post-processing) */
| 'preserve-token'
/** `:root{--x:...}` + `fill="var(--x)"` */
| 'css-var';Você também pode usar o modo de variáveis CSS: os globais são declarados em :root, para que você possa ajustá-los no devtools. Veja Using CSS custom properties (variables)
<svg>
<defs>
<style>
:root {
--color-background: #2563eb;
--color-stroke: red;
--text-title: 72px;
}
</style>
</defs>
<rect
fill="var(--color-background)"
stroke="var(--color-stroke)"
stroke-width="2"
width="100"
height="100"
id="node-r1"
/>
</svg>Ícone
Os ícones importam ao gerar interfaces. O Lucide já é amplamente usado na geração de código React, e o Pencil suporta gráficos embutidos semelhantes:
export interface IconFont extends Entity, Size, CanHaveEffects {
type: 'icon_font';
/** Name of the icon in the icon font */
iconFontName?: StringOrVariable;
/** Icon font to use. Valid fonts are 'lucide', 'feather', 'Material Symbols Outlined', 'Material Symbols Rounded', 'Material Symbols Sharp', 'phosphor' */
iconFontFamily?: StringOrVariable;
/** Variable font weight, only valid for icon fonts with variable weight. Values from 100 to 700. */
weight?: NumberOrVariable;
fill?: Fills;
}No OpenPencil, iconLookup é uma função injetável, o que significa:
- Flexibilidade: qualquer fonte de ícones (Iconify, Lucide, personalizada)
- Carga do modelo: o modelo só produz
iconFontName: "SearchIcon"; o runtime resolve os paths
private drawIconFont(canvas, node, x, y, w, h, opacity) {
const iconName = iNode.iconFontName ?? iNode.name ?? '';
const iconMatch = this.iconLookup?.(iconName) ?? null;
const iconD = iconMatch?.d ?? FALLBACK_ICON_D; // SVG path data
const iconStyle = iconMatch?.style ?? 'stroke'; // stroke or fill
// Resolve SVG path → Skia path → scale → draw
}Nossas definições:
export interface IconFontAttributes {
/** Name of the icon in the font family */
iconFontName?: StringOrVariable;
/**
* Font family, e.g. 'lucide', 'feather', 'Material Symbols Outlined', 'phosphor', etc.
*/
iconFontFamily?: StringOrVariable;
}
export interface IconFontSerializedNode
extends BaseSerializeNode<'iconfont'>,
Partial<IconFontAttributes>;Registrar ícone em tempo de execução
Usamos a forma do ícone de IconifyJSON para carregar Lucide, Material e outros conjuntos em tempo de execução por meio de JSON que inclui SVG:
import { registerIconifyIconSet } from '@infinite-canvas-tutorial/ecs';
const m = await import('@iconify/json/json/lucide.json');
registerIconifyIconSet('lucide', m);Os corpos dos ícones aceitam atributos SVG com aspas simples ou duplas. Paths, retângulos, círculos, elipses e linhas podem usar tags autocontidas ou tags de fechamento explícitas; os atributos fill e stroke de <g> aninhados são herdados pelas suas formas.
O JSON do ícone é mapeado para o nosso grafo de cena do mesmo modo que antes: um ícone Search vira um Group com filhos Path e Circle, de forma bem parecida com transformar SVG nas nossas formas.
"search": {
"body": "<g fill=\"none\" stroke=\"currentColor\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke-width=\"2\"><path d=\"m21 21l-4.34-4.34\"/><circle cx=\"11\" cy=\"11\" r=\"8\"/></g>"
}Também mapeamos largura, altura e strokeWidth no grafo convertido:
function buildIconFontScalablePrimitives(
iconFontName: string,
iconFontFamily: string,
targetWidth: number,
targetHeight: number,
): ScaledIconPrimitive[] {}Exibir no painel de camadas
O Iconify fornece Web Components para exibição. Veja Iconify Icon web component. Nós os usamos para as miniaturas na lista de camadas:
import 'iconify-icon';
if (this.node.type === 'iconfont') {
const iconName = this.#normalizeIconifyName(
this.node as IconFontSerializedNode,
);
thumbnail = iconName
? html`<iconify-icon icon=${iconName}></iconify-icon>`
: html`<sp-icon-group></sp-icon-group>`;
}Exportar SVG
Renderizar com layout
Junto com a Lição 33 - Motor de layout, você pode montar um botão que inclui um ícone:
const button1 = {
id: 'icon-button',
type: 'rect',
fill: 'grey',
display: 'flex',
width: 200,
height: 100,
padding: 10,
alignItems: 'center',
justifyContent: 'center',
flexDirection: 'row',
cornerRadius: 10,
gap: 10,
} as const;
const searchIcon = {
id: 'icon-button-search',
parentId: 'icon-button',
type: 'iconfont',
iconFontName: 'search',
iconFontFamily: 'lucide',
};
const text = {
id: 'icon-button-text',
parentId: 'icon-button',
type: 'text',
content: 'Button',
};Queremos um Button com várias variantes — no mesmo espírito do Shadcn UI — para não precisar escrever a mesma estrutura repetidas vezes:
<Button variant="secondary">Secondary</Button>
<Button variant="ghost">Ghost</Button>
<Button variant="destructive">Destructive</Button>Componentes e slots
Em arquivos .pen, o sistema ref + descendants é, na prática, um modelo de herança de componentes voltado para IA: o modelo não precisa entender a pilha de baixo nível (retângulo arredondado + texto + padding). Ele pode apontar para um round-button existente no design system e substituir o rótulo. É a mesma ideia de “herdar e depois substituir” que existe no código. Veja Components and Instances.
{
"id": "round-button",
"type": "g",
"reusable": true,
"cornerRadius": 9999,
"children": [
{
"id": "label",
"type": "text",
"content": "Submit",
"fill": "#000000"
...
}
]
}
{
"id": "save-round-button",
"type": "ref",
"ref": "round-button",
"descendants": { "label": { "content": "Save" } }
}Design ↔ código
A ideia central de Design ↔ Code é que design para código não é um chute de IA de pixel para código. Como o formato .ic já codifica primitivas amigáveis ao código — layout flex, variáveis de design, componentes reusable/ref, nomes semânticos e fontes de ícones — o grafo de cena é uma IR feita para código. Assim, o núcleo é um transpilador determinístico, e a IA só ajuda na camada de nomes e de limpeza da estrutura.
Pipeline
Espelhamos o pipeline de exportação SVG, mas inserimos uma IR de código independente de framework, de modo que adicionar um novo framework de destino seja apenas mais um emissor:
.ic SceneGraph
→ expandRefSerializedNodes // reuse existing ref/reusable handling
→ CodeIR // element tree + structured style + token refs + component defs
→ Emitter // react-tailwind | html-css | ...O nó da IR de código conserva: um papel (contêiner / texto / ícone / imagem / forma), referências $token pré-resolvidas, estilo flex estruturado e relações de componente reusable/ref.
Ponto de entrada
API.exportCode é a contraparte de renderToSVG. Sem argumentos, ele transpila a cena inteira; passe nós para transpilar uma seleção.
const code = api.exportCode(undefined, {
framework: 'react-tailwind', // or 'html-css'
variablesMode: 'css-var', // 'resolved' | 'preserve-token' | 'css-var'
componentStructure: 'preserve', // 'preserve' | 'flatten'
});Você também pode chamar a função pura diretamente:
import { serializedNodesToCode } from '@infinite-canvas-tutorial/ecs';
const code = serializedNodesToCode(nodes, {
framework: 'react-tailwind',
variables,
});Mapeamento de conceitos
Nós da tela são convertidos em elementos de layout, texto, imagem ou SVG. reusable e ref correspondem a componentes e instâncias React; variáveis são exportadas conforme o modo escolhido. Consulte o mapeamento completo.
Modos de variável
Os três modos de variável acompanham a semântica da exportação SVG:
resolved(padrão): resolve$tokenpara literais (por exemplo,bg-[#FFFFFF]).css-var: emitevar(--token)para estilos (por exemplo,bg-[var(--color-bg)]). A exportação HTML/CSS inclui um bloco:root { … }; o React delimita as definições a cada componente exportado, inclusive componentes reutilizáveis renderizados sozinhos. Instâncias aninhadas compartilham o escopo do pai, então exportações separadas podem usar valores diferentes para o mesmo token. Tokens de conteúdo de texto resolvem para os valores atuais porque propriedades customizadas de CSS não podem substituir nós de texto.preserve-token: mantém$token(encaminhado para umstyleinline para pós-processamento).
Componentes e instâncias
Com componentStructure: 'preserve' (padrão), cada reusable é exportado como componente React e cada ref como instância. Alterações compatíveis de texto, estilo e layout viram props; outras expandem a instância para preservar sua aparência.
Consulte as regras para texto, imagens, preenchimentos, Path, recorte e mistura nos detalhes de exportação.
interface RoundButtonProps {
label?: string;
}
export function RoundButton({ label = 'Submit' }: RoundButtonProps) {
return (
<div className="flex items-center justify-center w-[120px] h-[40px] rounded-[9999px]">
<span>{label}</span>
</div>
);
}
export function Design() {
return <RoundButton label="Save" />;
}componentStructure: 'flatten' é a alternativa: ele expande cada instância via expandRefSerializedNodes em DOM concreto, sem definições de componente. O emissor HTML/CSS sempre achata, já que HTML não tem o conceito de componente.
Exemplos práticos de interface
Experimente um painel de dados, uma página de preços e uma tela de checkout móvel na tela do editor IC. Edite os nós e gere, copie ou baixe código da tela atual. Selecione HTML/CSS e use HTML preview para conferir o resultado. A página de preços usa um cartão reutilizável.
A SaaS workspace with navigation, KPIs, a revenue chart and recent activity.
Baixe .ic para continuar editando no aplicativo online ou abra o HTML no navegador. Para React, baixe o projeto, extraia o ZIP e execute pnpm install e pnpm dev. O código gerado é a camada de apresentação; conecte navegação, pagamentos e dados na sua aplicação. Consulte os detalhes de exportação.
Linha de comando
O repositório também inclui uma CLI offline para arquivos .ic (Node.js 20+). Exporte o design a partir do editor com Exportar como… > .ic e salve-o como design.ic. A partir da raiz do repositório, compile as dependências do workspace nesta ordem e, em seguida, execute:
corepack pnpm install --frozen-lockfile
corepack pnpm --filter @infinite-canvas-tutorial/device-api build:cjs
corepack pnpm --filter @infinite-canvas-tutorial/ecs build:cjs
corepack pnpm --filter @infinite-canvas-tutorial/cli build
node packages/cli/dist/cli.js validate design.ic
node packages/cli/dist/cli.js inspect design.ic --json
node packages/cli/dist/cli.js codegen design.ic --framework react-tailwind --variables-mode css-var --component-name Design --output Design.tsx
node packages/cli/dist/cli.js codegen design.ic --framework html-css --output design.htmlinspect --json informa contagens de nós e de raízes, nós reutilizáveis, variáveis e contagens de URLs de assets. Omitir --output imprime o código gerado na stdout; documentos ou opções inválidos retornam um status de saída diferente de zero com uma mensagem de erro. A CLI produz o mesmo componente React ou fragmento HTML/CSS que o gerador de código do editor. Ela usa as variáveis e o tema do documento, não acessa um projeto online e rejeita imagens blob: temporárias que não sobrevivem fora do navegador. Use data URLs ou URLs de imagem duráveis nos documentos exportados.
Inverso: código → design
A direção inversa (analisar a AST de JSX/HTML de volta para nós .ic) é mais difícil e mais ambígua, então fica para uma segunda fase. O primeiro marco recomendado é uma ida e volta idempotente do código que este transpilador emite (design → código → design sem perda de informação) antes de estender a código arbitrário escrito à mão.