brustjs 0.1.28-alpha → 0.1.30-alpha

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.
@@ -0,0 +1,229 @@
1
+ // Native leaf route "/" — the landing page. Renders into AppLayout's <Outlet/>
2
+ // slot (chrome lives in AppLayout). SINGLE return, NO local bindings — a local
3
+ // `const` would soft-fall-back to an SSR component and lose the document shell.
4
+ //
5
+ // Everything dynamic is precomputed in homeLoader: `featured` cards, `typeTiles`
6
+ // (each carrying its own hex `color` for an inline style, since Tailwind's
7
+ // scanner can't see runtime-built type-color classes). The hero search box is
8
+ // the HeroSearch native directive (imperative navigate() dogfood).
9
+ import {
10
+ ArrowRight,
11
+ Box,
12
+ Component,
13
+ Cookie,
14
+ Database,
15
+ Layout,
16
+ List,
17
+ Navigation,
18
+ Server,
19
+ Table,
20
+ Zap,
21
+ } from 'lucide-react'
22
+ import HeroSearch from '../components/HeroSearch'
23
+ import type { HomeData } from '../lib/types'
24
+
25
+ export default function HomePage({ featured, typeTiles }: HomeData) {
26
+ return (
27
+ <div className="space-y-16 py-4">
28
+ <section className="relative overflow-hidden rounded-3xl bg-gradient-to-br from-brand-500 via-brand-600 to-indigo-700 px-6 py-16 text-center shadow-xl sm:px-12">
29
+ <span className="inline-flex items-center rounded-full bg-white/15 px-3 py-1 text-xs font-semibold uppercase tracking-wider text-white/90 ring-1 ring-white/20">
30
+ Built with brust
31
+ </span>
32
+ <h1 className="mx-auto mt-5 max-w-2xl text-4xl font-extrabold tracking-tight text-white sm:text-5xl">
33
+ Every Pokémon, one fast native-rendered Pokédex.
34
+ </h1>
35
+ <p className="mx-auto mt-4 max-w-xl text-base text-white/85 sm:text-lg">
36
+ Browse the National Dex, study the type chart, and build your dream team — server-rendered
37
+ in Rust, hydrated only where it counts.
38
+ </p>
39
+ <HeroSearch native />
40
+ <div className="mt-6 flex flex-wrap items-center justify-center gap-3">
41
+ <a
42
+ href="/pokedex"
43
+ className="inline-flex items-center gap-2 rounded-xl bg-white px-5 py-2.5 text-sm font-semibold text-brand-700 no-underline shadow-sm transition-transform hover:-translate-y-0.5"
44
+ >
45
+ Browse Pokédex
46
+ <ArrowRight size={16} isr={{ key: 'LcIconArrowRight' }} />
47
+ </a>
48
+ <a
49
+ href="/type-chart"
50
+ className="inline-flex items-center gap-2 rounded-xl bg-white/15 px-5 py-2.5 text-sm font-semibold text-white no-underline ring-1 ring-white/30 transition-colors hover:bg-white/25"
51
+ >
52
+ <Table size={16} isr={{ key: 'LcIconTable' }} />
53
+ Type chart
54
+ </a>
55
+ </div>
56
+ </section>
57
+
58
+ <section>
59
+ <div className="mb-5 flex items-end justify-between">
60
+ <h2 className="text-2xl font-extrabold tracking-tight text-slate-900 dark:text-white">
61
+ Featured
62
+ </h2>
63
+ <a
64
+ href="/pokedex"
65
+ className="inline-flex items-center gap-1 text-sm font-semibold text-brand-600 no-underline hover:underline dark:text-brand-50"
66
+ >
67
+ View all
68
+ <ArrowRight size={14} isr={{ key: 'LcIconArrowRight14' }} />
69
+ </a>
70
+ </div>
71
+ <div className="grid grid-cols-2 gap-3 sm:grid-cols-4">
72
+ {featured.map((p) => (
73
+ <a
74
+ key={p.id}
75
+ href={p.detailHref}
76
+ className="group flex flex-col items-center rounded-2xl border border-slate-200 bg-white p-4 no-underline shadow-sm transition-all hover:-translate-y-0.5 hover:border-brand-500/50 hover:shadow-md dark:border-slate-800 dark:bg-slate-900"
77
+ >
78
+ <span className="self-start text-[11px] font-semibold tabular-nums text-slate-400">
79
+ {p.num}
80
+ </span>
81
+ <img
82
+ src={p.artwork}
83
+ alt={p.displayName}
84
+ loading="lazy"
85
+ className="h-28 w-28 object-contain transition-transform group-hover:scale-110"
86
+ />
87
+ <div className="mt-1 text-sm font-semibold text-slate-800 dark:text-slate-100">
88
+ {p.displayName}
89
+ </div>
90
+ </a>
91
+ ))}
92
+ </div>
93
+ </section>
94
+
95
+ <section>
96
+ <h2 className="mb-5 text-2xl font-extrabold tracking-tight text-slate-900 dark:text-white">
97
+ Browse by type
98
+ </h2>
99
+ <div className="grid grid-cols-3 gap-2.5 sm:grid-cols-6">
100
+ {typeTiles.map((t) => (
101
+ <a
102
+ key={t.name}
103
+ href={t.href}
104
+ style={{ background: t.color }}
105
+ className="flex items-center justify-center rounded-xl px-3 py-2.5 text-sm font-semibold text-white no-underline shadow-sm transition-transform hover:-translate-y-0.5"
106
+ >
107
+ {t.label}
108
+ </a>
109
+ ))}
110
+ </div>
111
+ </section>
112
+
113
+ <section className="overflow-hidden rounded-3xl border border-slate-200 bg-white p-8 shadow-sm dark:border-slate-800 dark:bg-slate-900">
114
+ <div className="flex flex-col items-start gap-4 sm:flex-row sm:items-center sm:justify-between">
115
+ <div>
116
+ <h2 className="text-2xl font-extrabold tracking-tight text-slate-900 dark:text-white">
117
+ Build your team
118
+ </h2>
119
+ <p className="mt-2 max-w-md text-slate-500 dark:text-slate-400">
120
+ Add up to six Pokémon from any detail page. Your team lives in the floating dock and
121
+ follows you across the whole site.
122
+ </p>
123
+ </div>
124
+ <a
125
+ href="/pokedex"
126
+ className="inline-flex shrink-0 items-center gap-2 rounded-xl bg-brand-500 px-5 py-2.5 text-sm font-semibold text-white no-underline shadow-sm transition-colors hover:bg-brand-600"
127
+ >
128
+ Start picking
129
+ <ArrowRight size={16} isr={{ key: 'LcIconArrowRight' }} />
130
+ </a>
131
+ </div>
132
+ </section>
133
+
134
+ <section>
135
+ <h2 className="mb-1 text-2xl font-extrabold tracking-tight text-slate-900 dark:text-white">
136
+ Built with brust
137
+ </h2>
138
+ <p className="mb-6 max-w-2xl text-slate-500 dark:text-slate-400">
139
+ This whole site is one brust app. Every box below is a real framework feature dogfooded
140
+ right here.
141
+ </p>
142
+ <div className="grid grid-cols-1 gap-3 sm:grid-cols-2 lg:grid-cols-3">
143
+ <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
144
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-900 dark:text-white">
145
+ <Server size={16} className="text-brand-500" isr={{ key: 'LcIconServer' }} />
146
+ Native SSR routes
147
+ </div>
148
+ <div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
149
+ Every page is JSX compiled to minijinja and rendered in Rust.
150
+ </div>
151
+ </div>
152
+ <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
153
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-900 dark:text-white">
154
+ <Database size={16} className="text-brand-500" isr={{ key: 'LcIconDatabase' }} />
155
+ Loaders + ISR
156
+ </div>
157
+ <div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
158
+ Loaders fetch PokeAPI and precompute the view-model; cached responses stay fast.
159
+ </div>
160
+ </div>
161
+ <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
162
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-900 dark:text-white">
163
+ <Cookie size={16} className="text-brand-500" isr={{ key: 'LcIconCookie' }} />
164
+ Cookies / request-context
165
+ </div>
166
+ <div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
167
+ The theme toggle reads and writes the <code>mode</code> cookie per request.
168
+ </div>
169
+ </div>
170
+ <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
171
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-900 dark:text-white">
172
+ <Box size={16} className="text-brand-500" isr={{ key: 'LcIconBox' }} />
173
+ defineStore
174
+ </div>
175
+ <div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
176
+ The team dock is a window-singleton store shared across islands.
177
+ </div>
178
+ </div>
179
+ <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
180
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-900 dark:text-white">
181
+ <Navigation size={16} className="text-brand-500" isr={{ key: 'LcIconNavigation' }} />
182
+ Client navigation
183
+ </div>
184
+ <div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
185
+ NavLink active state, NavPreloader prefetch, and imperative navigate() in the hero
186
+ search.
187
+ </div>
188
+ </div>
189
+ <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
190
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-900 dark:text-white">
191
+ <Component size={16} className="text-brand-500" isr={{ key: 'LcIconComponent' }} />
192
+ React islands
193
+ </div>
194
+ <div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
195
+ The team dock hydrates as a React island while the page stays native.
196
+ </div>
197
+ </div>
198
+ <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
199
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-900 dark:text-white">
200
+ <List size={16} className="text-brand-500" isr={{ key: 'LcIconList' }} />
201
+ Keyed x-for
202
+ </div>
203
+ <div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
204
+ The browse grid filters and sorts live with a keyed, react-free reconcile.
205
+ </div>
206
+ </div>
207
+ <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
208
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-900 dark:text-white">
209
+ <Layout size={16} className="text-brand-500" isr={{ key: 'LcIconLayout' }} />
210
+ Nested Outlet layout
211
+ </div>
212
+ <div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
213
+ The navbar, breadcrumb, and footer are written once and nested over every route.
214
+ </div>
215
+ </div>
216
+ <div className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm dark:border-slate-800 dark:bg-slate-900">
217
+ <div className="flex items-center gap-2 text-sm font-bold text-slate-900 dark:text-white">
218
+ <Zap size={16} className="text-brand-500" isr={{ key: 'LcIconZap' }} />
219
+ Treaty actions
220
+ </div>
221
+ <div className="mt-1 text-sm text-slate-500 dark:text-slate-400">
222
+ Add-to-team and theme persistence call type-safe server actions over treaty.
223
+ </div>
224
+ </div>
225
+ </div>
226
+ </section>
227
+ </div>
228
+ )
229
+ }
@@ -1,42 +1,61 @@
1
- // Route "/type-chart" — NATIVE leaf route, rendered into AppLayout's <Outlet/>
2
- // slot (chrome lives in AppLayout). Returns JUST its inner aa-content fragment.
3
- // A static 18×18 type-effectiveness matrix: pure read-only data, the ideal
4
- // native page (compiled to jinja, rendered in Rust, zero React on the server).
5
- // The 19×19 grid uses nested `.map()` on the native path: rows.map(r =>
6
- // r.cells.map(c => …)) into a CSS grid.
1
+ // Native leaf route "/type-chart" — the 18×18 type-effectiveness matrix.
2
+ // Renders into AppLayout's <Outlet/> slot (chrome lives in AppLayout). SINGLE
3
+ // return, NO local bindings. Pure read-only data the ideal native page
4
+ // (compiled to jinja, rendered in Rust, zero React on the server).
5
+ //
6
+ // The 19×19 grid uses nested `.map()`: rows.map(r => r.cells.map(c => …)) into a
7
+ // CSS grid. Each row wrapper is `display:contents` (Tailwind `contents`) so
8
+ // every cell is a direct grid item — the row div leaves no box. Cell coloring
9
+ // is a static Tailwind utility class string the loader picks per effectiveness
10
+ // (literals in a .ts file, so `@source` scans them). The header row + row heads
11
+ // are `sticky` (also set in the loader class strings).
7
12
  import type { TypeChartData } from '../lib/types'
8
13
 
9
14
  export default function TypeChart({ rows }: TypeChartData) {
10
15
  return (
11
- <>
12
- <div className="aa-page-header">
13
- <div>
14
- <h1 className="aa-page-header__title">Type chart</h1>
15
- <div className="aa-page-header__desc">
16
- Damage relations · row attacks column · rendered ฝั่ง Rust จาก jinja (native:true · ไม่มี
17
- React runtime ใน payload)
18
- </div>
19
- </div>
16
+ <section className="py-2">
17
+ <div className="mb-5">
18
+ <h1 className="text-3xl font-extrabold tracking-tight text-slate-900 dark:text-white">
19
+ Type chart
20
+ </h1>
21
+ <p className="mt-2 max-w-2xl text-slate-500 dark:text-slate-400">
22
+ Damage relations rows attack, columns defend. Rendered server-side in Rust from jinja
23
+ (native:true · no React runtime in the payload).
24
+ </p>
20
25
  </div>
21
26
 
22
- <div className="dex-tc-legend">
23
- <span className="dex-tc-legend__item">
24
- <span className="dex-tc__cell dex-tc__cell--super dex-tc__swatch">2</span> super effective
27
+ <div className="mb-4 flex flex-wrap items-center gap-4 text-sm text-slate-500 dark:text-slate-400">
28
+ <span className="inline-flex items-center gap-1.5">
29
+ <span className="flex h-5 w-5 items-center justify-center rounded bg-green-500/25 text-xs font-bold text-green-700 dark:text-green-300">
30
+ 2
31
+ </span>
32
+ super effective
25
33
  </span>
26
- <span className="dex-tc-legend__item">
27
- <span className="dex-tc__cell dex-tc__cell--weak dex-tc__swatch">½</span> not very
34
+ <span className="inline-flex items-center gap-1.5">
35
+ <span className="flex h-5 w-5 items-center justify-center rounded bg-red-500/20 text-xs font-bold text-red-700 dark:text-red-300">
36
+ ½
37
+ </span>
38
+ not very effective
28
39
  </span>
29
- <span className="dex-tc-legend__item">
30
- <span className="dex-tc__cell dex-tc__cell--none dex-tc__swatch">0</span> no effect
40
+ <span className="inline-flex items-center gap-1.5">
41
+ <span className="flex h-5 w-5 items-center justify-center rounded bg-slate-800/80 text-xs font-bold text-slate-200">
42
+ 0
43
+ </span>
44
+ no effect
31
45
  </span>
32
46
  </div>
33
47
 
34
- <div className="dex-tc-scroll">
35
- <div className="dex-tc">
48
+ <div className="overflow-x-auto rounded-2xl border border-slate-200 bg-white p-2 shadow-sm dark:border-slate-800 dark:bg-slate-900">
49
+ <div className="grid w-max grid-cols-[2.75rem_repeat(18,2.25rem)] gap-px">
36
50
  {rows.map((r) => (
37
- <div key={r.id} className="dex-tc__row">
51
+ <div key={r.id} className="contents">
38
52
  {r.cells.map((c) => (
39
- <div key={c.id} className={c.className} title={c.title}>
53
+ <div
54
+ key={c.id}
55
+ className={c.className}
56
+ title={c.title}
57
+ style={{ background: c.bg }}
58
+ >
40
59
  {c.content}
41
60
  </div>
42
61
  ))}
@@ -44,6 +63,6 @@ export default function TypeChart({ rows }: TypeChartData) {
44
63
  ))}
45
64
  </div>
46
65
  </div>
47
- </>
66
+ </section>
48
67
  )
49
68
  }
@@ -1,35 +1,24 @@
1
1
  import { defineRoutes } from 'brustjs/routes'
2
2
  import AppLayout from './components/AppLayout'
3
- import { detailLoader, listLoader, typeChartLoader } from './lib/loaders'
3
+ import { browseLoader, detailLoader, homeLoader, typeChartLoader } from './lib/loaders'
4
+ import HomePage from './pages/HomePage'
5
+ import BrowsePage from './pages/BrowsePage'
4
6
  import DetailPage from './pages/DetailPage'
5
- import ListPage from './pages/ListPage'
6
7
  import TypeChart from './pages/TypeChart'
7
8
 
8
9
  // Every route is `native: true` — each page is compiled from JSX to a minijinja
9
- // template at build time and rendered in Rust (no per-request React). The loader
10
- // runs in a Bun worker and its return value becomes the template scope. The only
11
- // React that ever boots in the browser is the islands (TeamBuilder /
12
- // AddToTeamButton). See ./FRAMEWORK-GAPS.md for what this costs.
13
- //
14
- // ROUTER-LEVEL LAYOUT (Approach a): the chrome (sidebar / topbar / team-dock) is
15
- // written ONCE in AppLayout, nested as the parent of the three leaf routes. Each
16
- // leaf renders into AppLayout's <Outlet/> slot. The compiler builds the synth
17
- // wrapper `<AppLayout native><Leaf native/></AppLayout>` per leaf and runs the
18
- // chain loaders top-down, shallow-merging into one flat jinja context. The leaf
19
- // loaders therefore also return the chrome fields (title/active/crumb/teamProps)
20
- // that AppLayout reads — see lib/loaders.ts and components/AppLayout.tsx.
10
+ // template at build time and rendered in Rust. The loader runs in a Bun worker
11
+ // and its return value becomes the template scope. The chrome (navbar / footer /
12
+ // team-dock) is written ONCE in AppLayout, nested as the parent of the four leaf
13
+ // routes; each leaf renders into AppLayout's <Outlet/> slot.
21
14
  export const routes = defineRoutes([
22
15
  {
23
16
  Component: AppLayout,
24
17
  native: true,
25
18
  children: [
26
- // List + pagination (query string via req.search, validated by hand in the loader).
27
- { path: '/', Component: ListPage, native: true, loader: listLoader },
28
-
29
- // Dynamic param {name} + a (non-streamed) evolution chain loaded in the loader.
19
+ { path: '/', Component: HomePage, native: true, loader: homeLoader },
20
+ { path: '/pokedex', Component: BrowsePage, native: true, loader: browseLoader },
30
21
  { path: '/pokemon/{name}', Component: DetailPage, native: true, loader: detailLoader },
31
-
32
- // Static 18×18 effectiveness matrix — the ideal native page.
33
22
  { path: '/type-chart', Component: TypeChart, native: true, loader: typeChartLoader },
34
23
  ],
35
24
  },
@@ -4,7 +4,7 @@
4
4
  // module-scope store imported by both would be duplicated into two instances
5
5
  // that never sync. `defineStore` resolves both bundles to ONE instance on
6
6
  // `window.__BRUST_STORES__['pokedex.team']`, so a write in one island is seen by
7
- // the other — no hand-rolled CustomEvent bus. (Replaces the old components/team-bus.ts.)
7
+ // the other — no hand-rolled CustomEvent bus.
8
8
  import { defineStore, signal } from 'brustjs/store'
9
9
  import type { TeamMember } from '../lib/types'
10
10
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "brustjs",
3
- "version": "0.1.28-alpha",
3
+ "version": "0.1.30-alpha",
4
4
  "description": "Bun + Rust SSR framework — React on the server, Rust everywhere else (napi cdylib + per-worker SharedArrayBuffer).",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -40,12 +40,12 @@
40
40
  "typescript": "^6.0.3"
41
41
  },
42
42
  "optionalDependencies": {
43
- "brustjs-darwin-x64": "0.1.28-alpha",
44
- "brustjs-darwin-arm64": "0.1.28-alpha",
45
- "brustjs-linux-x64-gnu": "0.1.28-alpha",
46
- "brustjs-linux-arm64-gnu": "0.1.28-alpha",
47
- "brustjs-linux-x64-musl": "0.1.28-alpha",
48
- "brustjs-linux-arm64-musl": "0.1.28-alpha"
43
+ "brustjs-darwin-x64": "0.1.30-alpha",
44
+ "brustjs-darwin-arm64": "0.1.30-alpha",
45
+ "brustjs-linux-x64-gnu": "0.1.30-alpha",
46
+ "brustjs-linux-arm64-gnu": "0.1.30-alpha",
47
+ "brustjs-linux-x64-musl": "0.1.30-alpha",
48
+ "brustjs-linux-arm64-musl": "0.1.30-alpha"
49
49
  },
50
50
  "peerDependencies": {
51
51
  "react": "^19.2.6",
@@ -57,6 +57,7 @@
57
57
  "@types/react": "^19.2.15",
58
58
  "@types/react-dom": "^19.2.3",
59
59
  "happy-dom": "^20.9.0",
60
+ "lucide-react": "^1.17.0",
60
61
  "react": "^19.2.6",
61
62
  "react-dom": "^19.2.6",
62
63
  "zod": "^4.4.3"