원클릭으로
gea-framework
// Use when creating Gea applications, components, stores, routes, SSR entry points, JSX templates, or working with Gea reactivity and DOM patching.
// Use when creating Gea applications, components, stores, routes, SSR entry points, JSX templates, or working with Gea reactivity and DOM patching.
Guide for using @geajs/ui — a Tailwind-styled, Zag.js-powered component library for the Gea framework. Use when building UIs with @geajs/ui components like Button, Select, Dialog, Tabs, Toast, or any pre-built component from the library.
Rebuild the website playground browser bundles after changing @geajs/core or @geajs/vite-plugin source. Use when building browser bundles, syncing the playground, or committing changes that affect the playground preview.
Step-by-step guide for migrating React applications to Gea — covering project setup, component conversion, state management, routing, styling, and known pitfalls. Use when converting an existing React codebase to Gea or when advising on migration strategy.
Rebuild the local js-framework-benchmark HTML report using the curated framework set for this repo, and avoid polluting the report with every framework in webdriver-ts/results. Use when the user asks to update the benchmark report, refresh webdriver-ts-results, regenerate the HTML results page, or compare Gea against vanilla, solid, vue, and react.
| name | gea-framework |
| description | Use when creating Gea applications, components, stores, routes, SSR entry points, JSX templates, or working with Gea reactivity and DOM patching. |
Gea is a lightweight, reactive JavaScript UI framework built on the principle that JS code should be simple and understandable. It compiles JSX into efficient DOM operations at build time via a Vite plugin, uses proxy-based stores for state management, and employs event delegation for all user interactions. There is no virtual DOM — the Vite plugin analyzes your JSX templates and generates surgical DOM patches that update only the elements that depend on changed state.
Gea introduces no new programming concepts. There are no signals, no hooks, no dependency arrays, and no framework-specific primitives. Stores are classes with state and methods. Components are classes with a template() method or plain functions. Computed values are getters. The compile-time Vite plugin is the only "magic" — it analyzes ordinary JavaScript and wires up reactivity invisibly, so you write regular OOP/functional code that is fully reactive under the hood.
Read reference.md in this skill directory for the full API surface and detailed examples.
A Store holds shared application state. Extend the Store class, declare reactive properties as class fields, and add methods that mutate them. The store instance is wrapped in a deep Proxy that tracks every property change and batches notifications via queueMicrotask.
import { Store } from '@geajs/core'
class CounterStore extends Store {
count = 0
increment() {
this.count++
}
decrement() {
this.count--
}
}
export default new CounterStore()
Key rules:
export default new MyStore()), not the class.this.count++ triggers reactivity automatically.push, pop, splice, sort, reverse, shift, unshift) are intercepted and produce fine-grained change events.append operation.Gea has two component styles. Both compile to the same internal representation.
Extend Component and implement a template(props) method that returns JSX. Class components can hold their own reactive properties — use them when you need local, transient UI state.
import { Component } from '@geajs/core'
export default class Counter extends Component {
count = 0
increment() { this.count++ }
decrement() { this.count-- }
template() {
return (
<div class="counter">
<span>{this.count}</span>
<button click={this.increment}>+</button>
<button click={this.decrement}>-</button>
</div>
)
}
}
Event handlers accept both method references (click={this.increment}) and arrow functions (click={() => this.increment()}). Use method references for simple forwarding; use arrow functions when passing arguments or composing logic.
Use class components when you need local component state or lifecycle hooks.
Export a default function that receives props and returns JSX. The Vite plugin converts it to a class component internally.
export default function Greeting({ name }) {
return <h1>Hello, {name}!</h1>
}
Use function components for stateless, presentational UI.
Props follow JavaScript's native value semantics:
No emit, no v-model, no callback props needed for object/array mutations. Deep nesting works the same way — as long as the same reference is passed down, reactivity propagates across the entire tree.
| Concern | Where it lives | When to use |
|---|---|---|
| Shared app data (todos, user, cart) | Store | Data accessed by multiple components or persisted across navigation |
| Derived / computed values | Store getters | Values calculated from store state |
| Local, transient UI state (editing mode, hover) | Component properties | Ephemeral state that no other component needs |
A class component can have both local state and read from external stores:
export default class TodoItem extends Component {
editing = false
editText = ''
template({ todo, onToggle, onRemove }) {
const { editing, editText } = this
return (
<li class={`todo-item ${editing ? 'editing' : ''}`}>
<span dblclick={this.startEditing}>{todo.text}</span>
{/* ... */}
</li>
)
}
}
Split state into domain-specific stores. Each store is an independent singleton.
flight-store.ts → step, boardingPass
options-store.ts → luggage, seat, meal
payment-store.ts → passengerName, cardNumber, paymentComplete
Stores can import and call each other:
class FlightStore extends Store {
startOver(): void {
this.step = 1
optionsStore.reset()
paymentStore.reset()
}
}
A root component reads from all relevant stores and passes data down as props:
export default class App extends Component {
template() {
const { step } = flightStore
const { luggage } = optionsStore
return <div>{step === 1 && <OptionStep selectedId={luggage} onSelect={id => optionsStore.setLuggage(id)} />}</div>
}
}
Gea includes a built-in client-side router. The router is a Store that reactively tracks path, params, query, and hash. Define routes with setRoutes or createRouter.
Create a bare Router in router.ts. Keep this file free of view imports to avoid circular dependencies (views import router, so router.ts must not import views).
// src/router.ts
import { Router } from '@geajs/core'
export const router = new Router()
Set routes in App.tsx where both the router and view components are available:
// src/App.tsx
import { Component, Outlet } from '@geajs/core'
import { router } from './router'
import AppShell from './views/AppShell'
import Home from './views/Home'
import About from './views/About'
import UserProfile from './views/UserProfile'
import NotFound from './views/NotFound'
router.setRoutes({
'/': {
layout: AppShell,
children: {
'/': Home,
'/about': About,
'/users/:id': UserProfile,
},
},
'*': NotFound,
})
export default class App extends Component {
template() {
return <Outlet />
}
}
Layouts receive the resolved child as a page prop:
export default class AppShell extends Component {
template({ page }: any) {
return (
<div class="app">
<nav>
<Link to="/" label="Home" />
<Link to="/about" label="About" />
</nav>
<main>{page}</main>
</div>
)
}
}
For simple apps without layouts/guards (no circular dependency risk), you can use createRouter directly in router.ts and render with <RouterView router={router} />:
import { createRouter } from '@geajs/core'
import Home from './views/Home'
import About from './views/About'
export const router = createRouter({
'/': Home,
'/about': About,
} as const)
import { Component, RouterView } from '@geajs/core'
import { router } from './router'
export default class App extends Component {
template() {
return <RouterView router={router} />
}
}
Guards are synchronous functions on route groups that control access. A guard returns:
true — proceed to the routestring — redirect to that pathComponent — render it instead of the routeimport authStore from './stores/auth-store'
const AuthGuard = () => {
if (authStore.isAuthenticated) return true
return '/login'
}
router.setRoutes({
'/login': Login,
'/': {
layout: AppShell,
guard: AuthGuard,
children: {
'/dashboard': Dashboard,
'/settings': Settings,
},
},
})
Guards on nested groups stack parent → child. Guards are intentionally synchronous — they check store state, not async APIs. For async checks, use created() in the component.
/about/users/:id — extracted as { id: '42' }* — matches any unmatched path'/old': '/new' — string values trigger a redirect'/admin': () => import('./views/Admin') — code-split route componentsRenders an <a> tag that navigates with history.pushState. Modifier keys (Cmd/Ctrl+click) open in a new tab. Active links receive data-active and an active class; pass exact to require an exact path match.
<Link to="/about" label="About" class="nav-link" />
import { router } from './router'
router.push('/about') // pushState
router.navigate('/about') // alias for push
router.replace('/login') // replaceState
router.back()
router.forward()
router.go(-2)
Function components receive matched params as props:
export default function UserProfile({ id }) {
return <h1>User {id}</h1>
}
Class components receive them via created(props) and template(props). Route params are also available on router.params.
router.isActive('/dashboard') // true if path starts with /dashboard
router.isExact('/dashboard') // true only for exact match
Use matchRoute for manual route matching:
import { matchRoute } from '@geajs/core'
const result = matchRoute('/users/:id', '/users/42')
// { path: '/users/42', pattern: '/users/:id', params: { id: '42' } }
RouterView renders the current route. Use it with a router prop for createRouter setups, or with an inline routes array for quick prototypes:
// With createRouter (recommended)
<RouterView router={router} />
// With inline routes (uses the singleton router)
<RouterView routes={[
{ path: '/', component: Home },
{ path: '/about', component: About },
{ path: '/users/:id', component: UserProfile },
]} />
Gea JSX differs from React JSX in several ways:
| Feature | Gea | React |
|---|---|---|
| CSS classes | class="foo" | className="foo" |
| Event handlers | click={fn} or onClick={fn} | onClick={fn}, onInput={fn}, onChange={fn} |
| Checked inputs | checked={bool} + change={fn} | checked={bool} + onChange={fn} |
| Conditional render | {cond && <Child />} | Same |
| Lists with keys | {arr.map(item => <Item key={item.id} />)} | Same |
| Dynamic classes | class={`btn ${active ? 'on' : ''}`} | Same (with className) |
Both native-style (click, change) and React-style (onClick, onChange) event attribute names are supported. Native-style is preferred by convention.
Supported event attributes include mouse, keyboard, form, drag, touch, pointer, wheel, scroll, animation, and transition events: click, dblclick, input, change, submit, keydown, keyup, blur, focus, pointerdown, pointerup, pointermove, touchstart, touchmove, touchend, wheel, scroll, dragstart, dragend, dragover, dragleave, drop, animationstart, animationend, transitionstart, transitionend, and related variants.
With @geajs/mobile: tap, longTap, swipeRight, swipeUp, swipeLeft, swipeDown.
Gea supports React-style inline style objects. The compiler converts static style objects to CSS strings at build time and generates runtime conversion for dynamic values:
// Static — compiled to a CSS string at build time (zero runtime cost)
<div style={{ backgroundColor: 'red', fontSize: '14px', fontWeight: 'bold' }}>
Styled content
</div>
// Dynamic — converted to cssText at runtime
<div style={{ color: this.textColor, opacity: this.isVisible ? 1 : 0 }}>
Dynamic styling
</div>
// String styles still work (passed through as-is)
<div style={`width:${this.width}px`}>Sized content</div>
CSS property names use camelCase (like React). The compiler converts them to kebab-case: backgroundColor → background-color, fontSize → font-size.
ref AttributeUse ref to get a direct reference to a DOM element after render:
export default class Canvas extends Component {
canvasEl = null
template() {
return (
<div class="canvas-wrapper">
<canvas ref={this.canvasEl} width="800" height="600"></canvas>
</div>
)
}
onAfterRender() {
const ctx = this.canvasEl.getContext('2d')
ctx.fillRect(0, 0, 100, 100)
}
}
The compiler emits a direct assignment that sets the component property to the DOM element after the template is cloned. Multiple refs on different elements are supported.
The Gea compiler throws clear errors for JSX patterns it cannot compile. These are caught at build time, not at runtime:
| Pattern | Error | Fix |
|---|---|---|
<div {...props} /> | Spread attributes not supported | Destructure and pass props individually |
<{DynamicTag} /> | Dynamic component tags not supported | Use conditional rendering ({isA ? <A /> : <B />}) |
{() => <div />} | Function-as-child not supported | Use render props with named attributes instead |
export function Foo() { return <div /> } | Named JSX component exports not supported | Use export default function |
<>{items.map(...)}</> | Fragments as .map() item roots not supported | Wrap each item in a single root element |
import MyApp from './my-app.jsx'
const app = new MyApp()
app.render(document.getElementById('app'))
Components are instantiated with new, then .render(parentEl) inserts them into the DOM.
The @geajs/mobile package extends Gea with mobile-oriented UI primitives:
Component that renders to document.body by default.tap, longTap, swipeRight, swipeLeft, swipeUp, swipeDown events.import { View, ViewManager } from '@geajs/mobile'
class HomeView extends View {
template() {
return (
<view>
<h1>Home</h1>
</view>
)
}
onActivation() {
/* called when view enters viewport */
}
}
const vm = new ViewManager()
const home = new HomeView()
vm.setCurrentView(home)
npm create gea@latest my-app
cd my-app
npm install
npm run dev
The create-gea package scaffolds a Vite project with TypeScript, a sample store, class and function components, and the Vite plugin pre-configured.
// vite.config.js
import { defineConfig } from 'vite'
import { geaPlugin } from '@geajs/vite-plugin'
export default defineConfig({
plugins: [geaPlugin()]
})
The @geajs/vite-plugin Vite plugin handles JSX transformation, reactivity wiring, event delegation generation, and HMR.
@geajs/ssr provides server rendering and hydration for Gea apps. Use handleRequest(App, options) for Fetch-style request handlers, hydrate(App, element, { storeRegistry }) on the client, and geaSSR() in Vite for development middleware. SSR can serialize registered stores, resolve route configs on the server, stream HTML, and inject head tags.
// server.ts
import { handleRequest } from '@geajs/ssr'
import App from './src/App'
import store from './src/store'
export default handleRequest(App, {
storeRegistry: { AppStore: store },
})
// src/main.ts
import { hydrate } from '@geajs/ssr/client'
import App from './App'
import store from './store'
hydrate(App, document.getElementById('app'), {
storeRegistry: { AppStore: store },
})
@geajs/ui is a Tailwind-styled, Zag.js-powered component library for Gea. It provides ~35 ready-to-use components: simple styled primitives (Button, Card, Input) and behavior-rich interactive widgets (Select, Dialog, Tabs, Toast). For full usage instructions, component API, and examples, see the gea-ui-components skill in skills/gea-ui-components/.
| Package | npm | Description |
|---|---|---|
@geajs/core | npm | Core framework — stores, components, reactivity, DOM patching |
@geajs/ui | npm | Tailwind + Zag.js component library — Button, Select, Dialog, Tabs, Toast, etc. |
@geajs/mobile | npm | Mobile UI primitives — views, navigation, gestures, layout |
@geajs/vite-plugin | npm | Vite plugin — JSX transform, reactivity wiring, HMR |
@geajs/ssr | npm | Server-side rendering, hydration, streaming, store serialization, Vite SSR middleware |
create-gea | npm | Project scaffolder (npm create gea@latest) |
gea-tools | — | VS Code / Cursor extension for Gea JSX code intelligence |
The gea-tools extension (in packages/gea-tools) provides:
click, input, change, etc.)Full documentation is in the docs/ directory of the repository, structured for GitBook: