01
O que é o Dev Mode
O Dev Mode te deixa escrever seu perfil cee.bio em JSX. É pra quem quer que a bio pareça um app, não uma lista de links — heróis animados, sub-páginas com rotas, layouts customizados. Se não é você, o editor visual em /dashboard/customize cobre tudo o mais.
Seu código roda dentro de um iframe sandboxed, sem acesso same-origin, sem cookies e sem rede além dos assets que você incorpora. O runtime expõe uma stdlib mínima: seis componentes e três utilitários. É só isso — sem <Badges />, sem <Music />, sem globais inesperados.
A superfície
- Componentes:
Card,Avatar,Bio,SocialLinks,Background,Link - Utilitários:
useUser(),useRoute(),navigate(to) - Imports:
react,react-dom,react-dom/client,cee+ qualquer arquivo relativo do seu projeto
Os visitantes não pagam pela sua tooling. O editor pré-compila seu código com esbuild-wasm; o perfil público entrega só o bundle final (≤ 200KB) — sem download de 12MB de wasm pros seus fãs. Um badge persistente "Custom por @user" é renderizado fora do iframe, no maior z-index possível. Tá ali pra que um atacante esperto não consiga renderizar uma chrome falsa do cee.bio pra phishing nos seus visitantes. Não dá pra esconder.
Limites duros
02
Primeiros passos
- Liga o Dev Mode em /dashboard/customize/dev.
- Seu projeto é multi-arquivo. O entry é
main.tsx; imports relativos funcionam, extensões.tsx/.ts/.jsx/.jssão auto-resolvidas, maisfolder/index.*. - Ao salvar dispara um compile esbuild no navegador (debounce 250ms). Se o Dev Mode tá on e a compilação falha, o editor recusa salvar — você não consegue publicar código quebrado.
- Os visitantes públicos recebem o bundle pré-compilado. Eles nunca baixam o esbuild-wasm.
O menor programa válido:
import { Card, Avatar, Bio, SocialLinks } from "cee";
export default function Profile() {
return (
<Card>
<Avatar />
<Bio />
<SocialLinks />
</Card>
);
}03
Referência de componentes
Cada componente lê do contexto do usuário quando faz sentido — você só passa props pra sobrescrever. Tudo é tolerante: dados ausentes renderizam nada em vez de quebrar.
<Card />
Container genérico. Layout flex, padding/radius/background tematizáveis, presets opcionais de hover/animação. Não lê dados do usuário.
<Card> ⤴ wrapper · container puro
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Anything you want inside the card. |
padding | number | 24 | Inner padding in px. |
radius | number | 16 | Border radius in px. Overridden by `rounded`. |
rounded | boolean | 'sm' | 'md' | 'lg' | 'xl' | 'full' | 'md' | Convenience shortcut for radius. true=16, full=9999, sm/md/lg/xl=8/16/24/32. |
background | string | 'rgba(255,255,255,0.04)' | CSS background. Overridden by `gradient`. |
gradient | [string,string] | { from, to, angle? } | — | Linear-gradient background. |
border | string | '1px solid rgba(255,255,255,0.08)' | CSS border shorthand. |
shadow | boolean | 'sm' | 'md' | 'lg' | 'xl' | string | false | Drop shadow preset or raw CSS box-shadow. |
glow | boolean | string | false | Outer glow halo. true uses user.primaryColor. |
glowIntensity | number | 20 | Glow radius in px (0–80). |
blur | number | 0 | backdrop-filter blur in px (pair with semi-transparent background). |
opacity | number | 1 | 0–1 opacity multiplier on the card. |
hover | 'none' | 'lift' | 'glow' | 'tilt' | 'scale' | 'none' | Built-in hover micro-interaction. |
hoverScale | number | 1.02 | Scale factor when hover='scale' or 'lift'. |
animate | 'none' | 'fade' | 'slide-up' | 'slide-down' | 'zoom' | 'flip' | 'none' | Mount entry animation (one-shot). |
animateDelay | number | 0 | Delay in ms before entry animation fires. |
width | number | string | — | Fixed width (number = px, string = any CSS unit). |
maxWidth | number | string | 440 | Max width cap. |
fullWidth | boolean | false | Stretch to 100% of parent. Overrides width/maxWidth. |
direction | 'row' | 'column' | 'column' | Flex direction for children. |
align | 'start' | 'center' | 'end' | 'stretch' | 'stretch' | align-items shortcut. |
justify | 'start' | 'center' | 'end' | 'between' | 'around' | 'start' | justify-content shortcut. |
gap | number | 0 | Flex gap between direct children in px. |
as | 'div' | 'section' | 'article' | 'main' | 'aside' | 'div' | Semantic tag for the wrapper. |
interactive | boolean | false | Adds cursor:pointer + focus ring; emits onPress. |
onPress | (e: MouseEvent) => void | — | Click handler. Keyboard activation via Enter/Space when interactive. |
style | CSSProperties | — | Escape hatch; merged last. |
<Card>
<Bio />
</Card><Card
blur={20}
background="rgba(255,255,255,0.06)"
border="1px solid rgba(255,255,255,0.12)"
rounded="xl"
shadow="lg"
glow
glowIntensity={36}
padding={32}
gap={16}
>
<Avatar size={120} ring />
<Bio size={18} align="center" />
</Card><Card
interactive
hover="lift"
animate="slide-up"
animateDelay={120}
onPress={() => navigate("/about")}
>
<Bio>Tap me</Bio>
</Card>Pegadinhas
gradient sobrescreve background; rounded sobrescreve radius; fullWidth ignora width/maxWidth. Os click handlers só disparam quando interactive é true (Enter/Espaço também).<Avatar />
Foto do usuário. Fallback pra user.avatar, depois /default-avatar.png. Sempre círculo por padrão; troca com shape ou rounded.
| Prop | Type | Default | Description |
|---|---|---|---|
src | string | — | Image URL. Falls back to user.avatar, then /default-avatar.png. |
size | number | 96 | Width/height in px. |
alt | string | user.displayName ?? user.username ?? '' | Accessibility text. |
shape | 'circle' | 'square' | 'rounded' | 'hex' | 'circle' | Frame shape. rounded=24px, hex uses clip-path. |
rounded | boolean | number | true | true=circle, false=square, number=exact border-radius. Overrides shape when set. |
border | string | number | 0 | Border. Number = width in px (uses primaryColor); string = full CSS border. |
borderColor | string | user.primaryColor | Border color when border is numeric. |
glow | boolean | string | false | Outer glow. true uses primaryColor. |
glowIntensity | number | 20 | Glow radius in px. |
ring | boolean | { color?, width?, offset? } | false | Offset ring around the avatar (Discord-style). |
status | 'online' | 'idle' | 'dnd' | 'offline' | null | null | Status dot in the bottom-right. |
shadow | boolean | 'sm' | 'md' | 'lg' | false | Drop shadow preset. |
hover | 'none' | 'scale' | 'tilt' | 'spin' | 'none' | Hover micro-interaction. |
hoverScale | number | 1.05 | Scale on hover when hover='scale'. |
fallback | string | '/default-avatar.png' | URL used when src and user.avatar are both empty. |
lazy | boolean | true | Set loading='lazy' on the <img>. |
decoding | 'sync' | 'async' | 'auto' | 'async' | decoding hint passed to <img>. |
onClick | (e: MouseEvent) => void | — | Click handler. |
style | CSSProperties | — | Escape hatch. |
<Avatar /><Avatar
size={140}
border={3}
ring={{ color: "#46FF6E", width: 2, offset: 4 }}
shadow="lg"
status="online"
/><Avatar
shape="hex"
size={160}
hover="spin"
glow
glowIntensity={32}
/>Pegadinhas
rounded ganha de shape quando ambos estão setados. lazy default é true — desabilita pro avatar principal acima da dobra pra evitar flash.<Bio />
Renderiza o texto da bio do usuário. Se children, user.bio E fallback estão todos vazios, retorna null (sem <p> nenhum).
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Override text. If omitted, reads from user.bio. |
size | number | 16 | font-size in px. |
color | string | 'rgba(255,255,255,0.75)' | Text color. |
as | 'p' | 'div' | 'span' | 'h1' | 'h2' | 'h3' | 'p' | Semantic tag. |
align | 'left' | 'center' | 'right' | 'justify' | 'left' | Text alignment. |
weight | 300 | 400 | 500 | 600 | 700 | 800 | 900 | 400 | font-weight. |
italic | boolean | false | font-style: italic. |
lineHeight | number | 1.5 | line-height multiplier. |
letterSpacing | number | 0 | letter-spacing in px. |
maxLines | number | — | Clamp to N lines with ellipsis (uses -webkit-line-clamp). |
gradient | [string,string] | { from, to, angle? } | — | Gradient fill via background-clip: text. |
glow | boolean | string | false | Text-shadow glow. true uses primaryColor. |
shadow | boolean | string | false | Plain text-shadow (non-glow). |
uppercase | boolean | false | text-transform: uppercase. |
typing | boolean | { speed?, loop?, cursor? } | false | Typewriter animation. Reads from user.bio if children empty. |
fallback | string | '' | Used if user.bio is empty and no children (instead of rendering null). |
style | CSSProperties | — | Escape hatch. |
<Bio /><Bio
as="h1"
size={42}
weight={800}
align="center"
gradient={["#46FF6E", "#11461D"]}
/><Bio
typing={{ speed: 40, cursor: true, loop: false }}
size={20}
align="center"
/>Pegadinhas
gradient usa background-clip: text — precisa de um color fallback visível pra browsers antigos (Safari < 14). Faz clamp com maxLines só em valores block-level de as.<Background />
Monta sua imagem/vídeo de background na raiz via uma camada persistente — navegar entre rotas NÃO remonta o background, então vídeos não reiniciam e imagens não piscam.
| Prop | Type | Default | Description |
|---|---|---|---|
children | ReactNode | — | Content rendered above the background layer. |
src | string | — | Image or video URL. Falls back to user.backgroundImage. |
blur | number | 0 | filter: blur(px) on the bg layer. |
dim | number | 0 | Black overlay 0–1 (legacy; prefer `overlay`). |
fit | 'cover' | 'contain' | 'fill' | 'tile' | 'cover' | object-fit / background-size mode. |
position | 'center' | 'top' | 'bottom' | 'left' | 'right' | string | 'center' | Image position; raw CSS strings also accepted. |
overlay | string | { color?, opacity?, gradient?, angle? } | — | Tint overlay above the image. Replaces `dim` for more control. |
brightness | number | 1 | CSS filter brightness multiplier. |
saturation | number | 1 | CSS filter saturate multiplier. |
contrast | number | 1 | CSS filter contrast multiplier. |
hueRotate | number | 0 | CSS filter hue-rotate in degrees. |
grayscale | number | 0 | 0–1 grayscale amount. |
loop | boolean | true | Loop the <video>. |
muted | boolean | true | Mute the <video>. Required for autoplay in most browsers. |
autoPlay | boolean | true | Autoplay the <video>. |
playbackRate | number | 1 | Video playback speed. |
parallax | boolean | number | false | Parallax scroll. Number = strength (0–1). |
kenBurns | boolean | false | Slow zoom/pan effect for stills (ignored on video). |
gradient | [string,string] | { from, to, angle? } | — | Layered gradient. Replaces background if src is empty. |
fixed | boolean | false | background-attachment: fixed. |
fallback | string | — | Image URL used when src AND user.backgroundImage are empty. |
style | CSSProperties | — | Escape hatch (applied to wrapper). |
<Background /><Background
src="/my-loop.mp4"
overlay={{ color: "#000", opacity: 0.45 }}
brightness={0.9}
saturation={1.2}
playbackRate={0.85}
/><Background
gradient={{ from: "#0f172a", to: "#46FF6E", angle: 135 }}
kenBurns
/>Pegadinhas
<Background> diferentes em rotas diferentes faz o último montado ganhar (last-in-wins via useLayoutEffect). Pra backgrounds por rota, monta um <Background> na raiz e troca o src com useRoute().<Link />
Navegação interna entre rotas dentro do seu perfil. Renderiza um <a>, mas intercepta o click pra chamar navigate(to) — Ctrl/Cmd/Shift/middle-click ainda abrem nova aba.
| Prop | Type | Default | Description |
|---|---|---|---|
to | string | — | Required. Must start with ?, #, /, or be empty (validated by parent). |
children | ReactNode | — | Link contents. |
variant | 'text' | 'button' | 'card' | 'pill' | 'ghost' | 'text' | Visual preset. |
size | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Padding/font scale (button/card/pill/ghost variants). |
color | string | 'inherit' | Text color. |
background | string | — | Background color (button/card/pill/ghost variants). |
rounded | boolean | number | false | Border radius shortcut. true=pill (9999), number=exact px. |
shadow | boolean | 'sm' | 'md' | 'lg' | false | Drop shadow preset. |
glow | boolean | string | false | Outer glow. |
hover | 'none' | 'underline' | 'scale' | 'lift' | 'glow' | 'fill' | 'underline' | Hover effect. |
hoverScale | number | 1.02 | Scale when hover='scale' or 'lift'. |
block | boolean | false | Full-width block link. |
icon | ReactNode | — | Leading icon slot. |
iconRight | ReactNode | — | Trailing icon slot. |
external | boolean | auto | Force target=_blank rel=noopener. Auto-detected for absolute http(s). |
underline | 'none' | 'always' | 'hover' | 'hover' | Underline behavior. |
disabled | boolean | false | Renders muted, blocks navigation/onClick. |
prefetch | boolean | false | Hint for internal navigation (no-op today, reserved). |
border | string | number | 0 | Border. Number = width in px. |
borderColor | string | user.primaryColor | Border color when border is numeric. |
onClick | (e: MouseEvent) => void | — | Click handler. Called before navigation. |
style | CSSProperties | — | Escape hatch. |
<Link to="/about">About</Link><Link
to="/projects"
variant="button"
size="lg"
rounded
shadow="md"
hover="lift"
background="#46FF6E"
color="#000"
>
See projects →
</Link>Validação
to precisa começar com ?, #, /, ou estar vazio. O pai silenciosamente ignora qualquer outro valor — javascript:, data: e URLs externas são bloqueadas. Pra links externos use um <a> normal com target="_blank".04
Dados & hooks
useUser() retorna esse shape — e só esse shape. Email, badges, flags de premium, músicas e outros campos privados são intencionalmente não expostos.
interface DevModeUser {
username: string;
displayName: string | null;
avatar: string | null;
bio: string | null;
backgroundImage: string | null;
primaryColor: string | null;
socialLinks: Array<{
platform?: string;
label?: string;
url: string;
isVisible?: boolean;
}>;
}Hook precisa rodar dentro da árvore do runtime
useUser() fora da árvore montada pelo runtime, ele lança o erro literal:useUser() chamado fora do <Profile>. Garanta que o componente raiz é renderizado pelo runtime.Em português: "useUser() chamado fora do <Profile>. Garanta que o componente raiz é renderizado pelo runtime."
import { useUser, useRoute, navigate } from "cee";
export default function Profile() {
const user = useUser(); // never null — throws if misused
const route = useRoute(); // { p: string | null, params: {...} }
return (
<div>
<h1>@{user.username}</h1>
<p>You're on: {route.p ?? "/"}</p>
<button onClick={() => navigate("/about")}>Go to about</button>
</div>
);
}useRoute() nunca lança — retorna { p: null, params: {} } por padrão. navigate(to) posta uma mensagem pro pai que valida o to contra /^[?#/]/ ou string vazia. URLs externas ou javascript: são silenciosamente descartadas.
05
Roteamento
Dois jeitos de dirigir perfis multi-rota:
- Single-page: só
export default function— seu componente cuida de toda a lógica de roteamento viauseRoute(). - Routes map:
export const routes = { ... }— o runtime escolhe o componente que casa pra você.
As rotas são dirigidas por ?p=<path> na URL pra funcionar num slug de perfil estático tipo cee.bio/yourname.
import { Card, Bio, Link, useRoute } from "cee";
function Home() {
return <Card><Bio /><Link to="/about">About →</Link></Card>;
}
function About() {
return <Card><h2>About me</h2><Link to="/">← Back</Link></Card>;
}
function NotFound() {
return <Card><p>Not found.</p><Link to="/">Home</Link></Card>;
}
export const routes = {
"/": Home,
"/about": About,
default: NotFound,
};Prioridade do pickComponent:
exports.routes['/' + key]depoisexports.routes[key]depoisexports.routes['/'](se não temp) depoisexports.routes.defaultexports.defaultexports.Profileexportsem si se for uma função
06
Exemplos & cookbook
Copia, cola, publica. Todos os snippets são arquivos main.tsx sintaticamente válidos.
Perfil mínimo
import { Card, Avatar, Bio, SocialLinks, Background } from "cee";
export default function Profile() {
return (
<>
<Background />
<Card padding={32} gap={16} align="center">
<Avatar size={120} ring />
<Bio size={18} align="center" />
<SocialLinks size={48} gap={14} />
</Card>
</>
);
}Card de vidro com blur fosco
import { Card, Avatar, Bio, SocialLinks, Background } from "cee";
export default function Profile() {
return (
<>
<Background brightness={0.7} saturation={1.1} />
<Card
blur={24}
background="rgba(255,255,255,0.06)"
border="1px solid rgba(255,255,255,0.15)"
rounded="xl"
shadow="xl"
glow
glowIntensity={40}
padding={36}
gap={20}
animate="fade"
align="center"
>
<Avatar size={140} border={2} />
<Bio as="h1" size={28} weight={700} align="center" />
<SocialLinks size={48} colorMode="brand" hover="lift" />
</Card>
</>
);
}Background de vídeo com tint de overlay
import { Card, Avatar, Bio, Background } from "cee";
export default function Profile() {
return (
<>
<Background
src="/loop.mp4"
overlay={{ gradient: ["#000", "rgba(70,255,110,0.3)"], angle: 200, opacity: 0.6 }}
playbackRate={0.8}
/>
<Card padding={40} align="center" gap={16}>
<Avatar size={160} ring glow />
<Bio
as="h1"
size={36}
weight={800}
align="center"
gradient={["#fff", "#46FF6E"]}
/>
</Card>
</>
);
}Grid de pills sociais com stagger
import { Card, SocialLinks } from "cee";
export default function Profile() {
return (
<Card padding={28} gap={20}>
<SocialLinks
direction="grid"
columns={3}
size={64}
gap={12}
shape="rounded"
colorMode="brand"
showLabel
labelPosition="bottom"
hover="bounce"
shadow="md"
animate="stagger"
animateDelay={70}
/>
</Card>
);
}Bio em máquina de escrever
import { Card, Avatar, Bio } from "cee";
export default function Profile() {
return (
<Card padding={32} align="center" gap={14}>
<Avatar size={110} />
<Bio
typing={{ speed: 35, cursor: true }}
size={22}
weight={600}
align="center"
/>
</Card>
);
}Layout duas colunas paired-profile
import { Card, Avatar, Bio, SocialLinks } from "cee";
export default function Profile() {
return (
<Card direction="row" gap={32} padding={32} fullWidth>
<Card padding={24} align="center" gap={12} maxWidth={300}>
<Avatar size={100} />
<Bio align="center" />
<SocialLinks size={36} />
</Card>
<Card padding={24} gap={12}>
<h2 style={{ fontSize: 20, fontWeight: 700, color: "#fff" }}>Notes</h2>
<Bio>Drop anything you want on the right column.</Bio>
</Card>
</Card>
);
}Overlay de entrada full-screen → revela card
import { useEffect, useState } from "react";
import { Card, Avatar, Bio, SocialLinks } from "cee";
export default function Profile() {
const [entered, setEntered] = useState(false);
useEffect(() => {
const t = setTimeout(() => setEntered(true), 1200);
return () => clearTimeout(t);
}, []);
if (!entered) {
return (
<div
style={{
position: "fixed", inset: 0, display: "grid", placeItems: "center",
background: "#000", zIndex: 50,
}}
>
<p style={{ fontSize: 14, letterSpacing: "0.4em", color: "#46FF6E" }}>
LOADING
</p>
</div>
);
}
return (
<Card animate="zoom" padding={32} gap={16} align="center">
<Avatar size={120} ring />
<Bio align="center" />
<SocialLinks />
</Card>
);
}Card de estatísticas estilo analytics
import { Card, Bio, useUser } from "cee";
function Stat({ label, value }) {
return (
<Card padding={16} align="center" maxWidth={140} gap={4} background="rgba(70,255,110,0.06)">
<Bio as="span" size={26} weight={800} color="#46FF6E">{value}</Bio>
<Bio as="span" size={11} uppercase letterSpacing={1.5}>{label}</Bio>
</Card>
);
}
export default function Profile() {
const u = useUser();
return (
<Card padding={28} gap={16}>
<Bio as="h1" size={28} weight={800}>@{u.username}</Bio>
<Card direction="row" gap={12} fullWidth>
<Stat label="Songs" value="124" />
<Stat label="Drops" value="8" />
<Stat label="Fans" value="2.1k" />
</Card>
</Card>
);
}Tab switcher consciente da rota
import { Card, Link, useRoute } from "cee";
function Tab({ to, label, active }) {
return (
<Link
to={to}
variant={active ? "button" : "ghost"}
rounded
size="sm"
background={active ? "#46FF6E" : undefined}
color={active ? "#000" : "#fff"}
>
{label}
</Link>
);
}
export default function Profile() {
const { p } = useRoute();
const tab = p ?? "/";
return (
<Card padding={24} gap={16}>
<Card direction="row" gap={8}>
<Tab to="/" label="Home" active={tab === "/"} />
<Tab to="/links" label="Links" active={tab === "/links"} />
<Tab to="/about" label="About" active={tab === "/about"} />
</Card>
<div>
{tab === "/" && <p>Home content.</p>}
{tab === "/links" && <p>Links content.</p>}
{tab === "/about" && <p>About content.</p>}
</div>
</Card>
);
}07
Imports & resolução de módulos
O que dá pra importar:
react,react-dom,react-dom/client— ligados ao React vendorizado do runtime.cee— a stdlib (componentes + hooks +navigate+React).- Qualquer caminho relativo dentro do seu projeto:
./Hero,../lib/format,./pages/About.tsx.
Imports bare de npm (lodash, framer-motion, etc.) falham em tempo de compilação. Sem esm.sh, sem unpkg — por design, pra manter a supply chain trancada.
Fallback de extensão: match exato → .tsx → .ts → .jsx → .js → folder/index.*.
// main.tsx
import { Card } from "cee";
import { Hero } from "./Hero";
import { fmt } from "./lib/format";
export default function Profile() {
return <Card><Hero label={fmt("hi")} /></Card>;
}
// Hero.tsx
export function Hero({ label }) { return <h1>{label}</h1>; }
// lib/format.ts (resolved as lib/format → lib/format.ts)
export function fmt(s: string) { return s.toUpperCase(); }Limites duros
08
Problemas comuns
Esperava `export default function ...` ou `export const routes = {...}` no seu código.
O runtime montou mas não achou nada pra renderizar. Adiciona um export default function Profile(), um export function Profile(), ou um export const routes = { ... }.
useUser() chamado fora do <Profile>. Garanta que o componente raiz é renderizado pelo runtime.
Você chamou useUser() de um helper de escopo de módulo ou antes do runtime te montar. Move a chamada pra dentro de um componente que o runtime vai renderizar (é o que retorna <Profile /> de routes ou do export default).
Bundle muito grande (compilado > 200KB)
Sua saída CJS passou de 200KB. Divide arquivos, tira blobs grandes de dados inline (move pra fetches em runtime se precisar) e evita copiar libs no seu projeto — não tem tree-shaking além de eliminação de código morto, e o React já é provido externamente.
Recuperação de draft no localStorage
O editor autosalva seu projeto no localStorage a cada ~400ms. Se você fechar a aba no meio da edição, seu draft é restaurado na próxima abertura. Pra forçar um start limpo, limpa os dados do site ou usa janela anônima.
"Por que a tela tá cinza?"
Ou você não tem um default export, ou seu componente retornou null, ou seu CSS de background tá escondendo tudo. Abre o DevTools → checa o console por uma mensagem cee:error.
"Background não muda entre rotas"
By design. O background vive fora da árvore de rotas pra vídeos não reiniciarem. Pra backgrounds por rota, monta um <Background> na raiz e troca o src baseado em useRoute().p.
"Expected object, received null"
/users/update ao salvar um preset. Reporta via Discord ou pelo formulário de contato — inclui o payload da request e o body da response.09
Segurança & limites
- iframe sandbox:
allow-scripts allow-popups— semallow-same-origin, então o iframe ganha uma origin opaca. Cookies e storage isolados; não consegue ler a auth do cee.bio. - Permissions-Policy:
allow=""zera todas as features delegadas (câmera, mic, geolocalização, payment, USB, etc). - Referrer:
referrerPolicy="no-referrer"no iframe. - Badge anti-phishing persistente: "Custom por @user" é renderizado FORA do iframe no z-index
2147483647. Código do usuário não consegue desenhar por cima nem falsificar a chrome do cee.bio. - Validação de navegação:
<Link to="...">é repassado pro pai que re-valida contra/^[?#/]/ou vazio. URLsjavascript:/data:/externas são derrubadas na camada pai mesmo se você passar do filho. - CSP + React vendorizado: O React é servido do mesmo origin em
/vendor/devmode-react.js. Sem esm.sh, sem unpkg, sem supply chain de CDN. - Origin de mensagem cross-frame: os dois lados forçam
ev.source === window.parent/iframeRef.current?.contentWindow— defesa em profundidade.
Se você encontrar um problema (XSS-no-sandbox, cover-up do badge, spoof de mensagem do pai, etc.), por favor reporta via Discord. Não abre issue público antes da gente ter chance de corrigir.
10
Referência: erros & mensagens
Tipos de postMessage
| Tipo | Direção | Payload |
|---|---|---|
| cee:ready | iframe → parent | (none) — iframe is alive |
| cee:mount | parent → iframe | { code, user, route } |
| cee:mounted | iframe → parent | (none) — mount succeeded |
| cee:user | parent → iframe | { user } (live update) |
| cee:route | parent → iframe | { route } (live update) |
| cee:error | iframe → parent | { message } (clipped to 4000 chars) |
| cee:navigate | iframe → parent | { to } (parent re-validates) |
| cee:hello | parent → iframe | (handshake, optional) |
Strings de erro de runtime (literais)
Todos os erros são em português; explicação em inglês abaixo de cada um.
Esperava `export default function ...` ou `export const routes = {...}` no seu código.
EN: Expected `export default function ...` or `export const routes = {...}` in your code. — pickComponent não achou nada renderizável.
useUser() chamado fora do <Profile>. Garanta que o componente raiz é renderizado pelo runtime.
EN: useUser() called outside <Profile>. Ensure your root component is rendered by the runtime.
Bundle muito grande
EN: Bundle too large. — O CJS compilado passou de 200KB; o iframe recusou montar.
11
CLI local: @cee.bio/cli
Editor local com hot reload, push & pull do seu projeto Dev Mode.
Pra quem prefere editar localmente com seu próprio editor (VSCode, Cursor, Neovim), publicamos um CLI em @cee.bio/cli. Ele dá scaffold de um projeto baseado em Vite + React, sincroniza com o cee.bio e tem hot reload com a preview do seu profile real (não mock).
Scaffold (1 comando)
Cria um projeto novo num diretório vazio. Não precisa instalar nada global — o npx resolve. O scaffold já gera .cee/project.json com um id estável: a partir do segundo push, o template é atualizado em vez de duplicado.
npx create-cee-profile@latest my-profile
cd my-profile
npm install
npm run login # vincula sua conta cee.bio (uma vez por máquina)
npm run dev # localhost:5173 com hot reloadComandos
npm run login— vincula sua conta cee.bio via pairing code: o CLI abre um link no browser, você confirma logado, e o token é gravado em~/.cee/credentials.json(chmod 600). Cada máquina ganha um token próprio, revogável independentemente.npm run dev— sobe o Vite local na porta 5173 com hot reload. Antes de subir, faz fetch do seu profile real (GET /cli/profile) e escreve emsrc/.cee-user.json: o preview renderiza com sua foto, bio, social_links e badges reais.npm run push— compila com esbuild (mesma config do editor in-site) e envia. Limites: 250KB source / 800KB bundle. Por padrão, salva o source como template no dashboard com upsert porprojectId(mesmo projeto = mesmo template, sempre atualizado). Use--no-templatepra pular ou--draftpra enviar sem ativar.npm run pull— baixa o source atual do servidor prasrc/profile.tsx. Útil pra começar a partir do que tá no ar, ou recuperar uma versão depois de mexer demais local.
# Sempre use `npm run X` — existe um pacote npm não-relacionado
# chamado "cee" que sequestra `npx cee`. O scaffold inclui
# @cee.bio/cli como devDependency, então `npm run login` resolve
# o binário local correto.
npm run login # vincula esta máquina (pairing code via browser)
npm run dev # Vite dev server + hot reload, com seu profile real
npm run push # builda + publica em cee.bio/seu-user
npm run pull # baixa o source atual do servidornpm run porque existe um pacote npm não-relacionado chamado cee que sequestra npx cee. O scaffold inclui @cee.bio/cli como devDependency, e npm run resolve o binário local correto. Mesmas validações e sandbox do editor web valem aqui — o bundle que entra em produção é idêntico.Pronto pra começar?
Tudo aqui vive atrás de um toggle. Liga ele e começa a escrever JSX — sem instalar nada, sem boilerplate, sem ejetar.
Abrir o editor →