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.
- package/example/pokedex/app.css +8 -1712
- package/example/pokedex/components/AddToTeamButton.tsx +36 -19
- package/example/pokedex/components/AppLayout.tsx +48 -50
- package/example/pokedex/components/Breadcrumb.tsx +49 -0
- package/example/pokedex/components/DexFilter.tsx +121 -0
- package/example/pokedex/components/HeroSearch.tsx +51 -0
- package/example/pokedex/components/NavLink.tsx +16 -23
- package/example/pokedex/components/NavPreloader.tsx +7 -3
- package/example/pokedex/components/TeamBuilder.tsx +48 -131
- package/example/pokedex/components/ThemeToggle.tsx +22 -11
- package/example/pokedex/lib/loaders.ts +125 -115
- package/example/pokedex/lib/pokeapi.ts +21 -21
- package/example/pokedex/lib/team-store.ts +1 -1
- package/example/pokedex/lib/types.ts +73 -94
- package/example/pokedex/pages/BrowsePage.tsx +31 -0
- package/example/pokedex/pages/DetailPage.tsx +176 -91
- package/example/pokedex/pages/HomePage.tsx +229 -0
- package/example/pokedex/pages/TypeChart.tsx +46 -27
- package/example/pokedex/routes.tsx +9 -20
- package/example/pokedex/stores/team.ts +1 -1
- package/package.json +8 -7
- package/runtime/cli/native-routes-emit.ts +223 -63
- package/runtime/cli/templates.ts +7 -4
- package/runtime/index.js +52 -52
- package/runtime/native/runtime.ts +160 -16
- package/example/pokedex/pages/ListPage.tsx +0 -76
|
@@ -1,11 +1,9 @@
|
|
|
1
|
-
// Route loaders. Each runs in a Bun worker (full JS),
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
// See ../FRAMEWORK-GAPS.md.
|
|
1
|
+
// Route loaders. Each runs in a Bun worker (full JS), and returns a fully
|
|
2
|
+
// render-ready view-model. SLICE 1: these are STUBS — they return the chrome
|
|
3
|
+
// fields every page needs (title / crumb / mode / teamProps) plus the minimal
|
|
4
|
+
// data each stub page interpolates. Real PokeAPI-backed data lands in later
|
|
5
|
+
// slices.
|
|
7
6
|
|
|
8
|
-
import { z } from 'zod'
|
|
9
7
|
import { type BrustRequest, type NativeVerdict, notFound } from 'brustjs/routes'
|
|
10
8
|
import {
|
|
11
9
|
ALL_TYPES,
|
|
@@ -22,35 +20,67 @@ import {
|
|
|
22
20
|
TYPE_COLOR,
|
|
23
21
|
} from './pokeapi'
|
|
24
22
|
import { teamStore } from './team-store'
|
|
25
|
-
import type {
|
|
23
|
+
import type {
|
|
24
|
+
BrowseData,
|
|
25
|
+
DetailData,
|
|
26
|
+
HomeData,
|
|
27
|
+
TypeBadgeVM,
|
|
28
|
+
TypeChartCellVM,
|
|
29
|
+
TypeChartData,
|
|
30
|
+
TypeChartRowVM,
|
|
31
|
+
} from './types'
|
|
32
|
+
|
|
33
|
+
/** A curated set of iconic Pokémon for the home featured strip. Hardcoded
|
|
34
|
+
* {id,name} so the home page needs ZERO PokeAPI calls — artwork is derived from
|
|
35
|
+
* id, names supply the display label and detail href. */
|
|
36
|
+
const FEATURED: { id: number; name: string }[] = [
|
|
37
|
+
{ id: 1, name: 'bulbasaur' },
|
|
38
|
+
{ id: 4, name: 'charmander' },
|
|
39
|
+
{ id: 7, name: 'squirtle' },
|
|
40
|
+
{ id: 25, name: 'pikachu' },
|
|
41
|
+
{ id: 39, name: 'jigglypuff' },
|
|
42
|
+
{ id: 94, name: 'gengar' },
|
|
43
|
+
{ id: 143, name: 'snorlax' },
|
|
44
|
+
{ id: 150, name: 'mewtwo' },
|
|
45
|
+
]
|
|
26
46
|
|
|
27
|
-
/** Loader context shape — `loader: ({ params, path, req }) => data`. */
|
|
28
47
|
interface LoaderCtx {
|
|
29
48
|
params: Record<string, string>
|
|
30
49
|
path: string
|
|
31
50
|
req: BrustRequest
|
|
32
51
|
}
|
|
33
52
|
|
|
34
|
-
const
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
// `req.search` as a raw Record<string,string> and must validate by hand here.
|
|
40
|
-
const ListQuery = z.object({
|
|
41
|
-
offset: z.coerce.number().int().min(0).max(NATIONAL_MAX).catch(0),
|
|
53
|
+
const chrome = (req: BrustRequest, title: string, crumb: string) => ({
|
|
54
|
+
title,
|
|
55
|
+
crumb,
|
|
56
|
+
mode: (req.cookies.mode === 'light' ? 'light' : 'dark') as 'light' | 'dark',
|
|
57
|
+
teamProps: { teamInitial: teamStore.list() },
|
|
42
58
|
})
|
|
43
59
|
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
60
|
+
export async function homeLoader({ req }: LoaderCtx): Promise<HomeData> {
|
|
61
|
+
const featured = FEATURED.map((p) => ({
|
|
62
|
+
id: p.id,
|
|
63
|
+
name: p.name,
|
|
64
|
+
displayName: cap(p.name),
|
|
65
|
+
num: pad(p.id),
|
|
66
|
+
artwork: artwork(p.id),
|
|
67
|
+
detailHref: `/pokemon/${p.name}`,
|
|
68
|
+
}))
|
|
69
|
+
const typeTiles = ALL_TYPES.map((t) => ({
|
|
70
|
+
name: t,
|
|
71
|
+
label: cap(t),
|
|
72
|
+
color: TYPE_COLOR[t] ?? '#888888',
|
|
73
|
+
href: '/pokedex',
|
|
74
|
+
}))
|
|
75
|
+
return {
|
|
76
|
+
...chrome(req, 'PokéDex · built with brust', 'Home'),
|
|
77
|
+
featured,
|
|
78
|
+
typeTiles,
|
|
79
|
+
}
|
|
80
|
+
}
|
|
53
81
|
|
|
82
|
+
export async function browseLoader({ req }: LoaderCtx): Promise<BrowseData> {
|
|
83
|
+
const { results } = await fetchList(0, 151)
|
|
54
84
|
const items = results.map((r) => ({
|
|
55
85
|
id: r.id,
|
|
56
86
|
name: r.name,
|
|
@@ -59,106 +89,79 @@ export async function listLoader({ req }: LoaderCtx): Promise<ListData> {
|
|
|
59
89
|
artwork: artwork(r.id),
|
|
60
90
|
detailHref: `/pokemon/${r.name}`,
|
|
61
91
|
}))
|
|
62
|
-
|
|
63
|
-
const lastPage = Math.ceil(total / PAGE)
|
|
64
|
-
const pageNo = Math.floor(offset / PAGE) + 1
|
|
65
|
-
const hasPrev = offset > 0
|
|
66
|
-
const hasNext = offset + PAGE < total
|
|
67
|
-
const prevOffset = Math.max(0, offset - PAGE)
|
|
68
|
-
|
|
69
92
|
return {
|
|
93
|
+
...chrome(req, 'Pokédex · Browse', 'Pokédex'),
|
|
70
94
|
items,
|
|
71
|
-
|
|
72
|
-
totalLabel: fmt(total),
|
|
73
|
-
offset,
|
|
74
|
-
offsetLabel: String(offset),
|
|
75
|
-
showingLabel: `${fmt(offset + 1)}–${fmt(Math.min(offset + PAGE, total))} of ${fmt(total)}`,
|
|
76
|
-
pageLabel: `${pageNo} / ${lastPage}`,
|
|
77
|
-
// Real conditionals now exist in native routes (GAPS S11 closed): the
|
|
78
|
-
// template branches on these booleans with `{flags.hasPrev ? <a/> : <span/>}`
|
|
79
|
-
// instead of always-rendering a loader-computed hide-class.
|
|
80
|
-
hasPrev,
|
|
81
|
-
hasNext,
|
|
82
|
-
prevHref: hasPrev ? (prevOffset > 0 ? `/?offset=${prevOffset}` : '/') : '#',
|
|
83
|
-
nextHref: hasNext ? `/?offset=${offset + PAGE}` : '#',
|
|
84
|
-
// Chrome fields (ChromeData) — merged into the flat jinja context and read by
|
|
85
|
-
// the router-level AppLayout (Approach a: native <Outlet/> nesting).
|
|
86
|
-
title: 'PokéDex · brust example',
|
|
87
|
-
active: 'list',
|
|
88
|
-
crumb: 'All Pokémon',
|
|
89
|
-
teamProps: { teamInitial: teamStore.list() },
|
|
90
|
-
mode: req.cookies.mode === 'light' ? 'light' : 'dark',
|
|
95
|
+
dexProps: JSON.stringify({ items }),
|
|
91
96
|
}
|
|
92
97
|
}
|
|
93
98
|
|
|
99
|
+
/** Base-stat bucket → bar fill hex. Bucket order: hi / mid / low / min. */
|
|
100
|
+
const BAR_COLOR: Record<string, string> = {
|
|
101
|
+
hi: '#16a34a', // green-600
|
|
102
|
+
mid: '#0ea5e9', // sky-500
|
|
103
|
+
low: '#f59e0b', // amber-500
|
|
104
|
+
min: '#ef4444', // red-500
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const typeBadge = (t: string): TypeBadgeVM => ({
|
|
108
|
+
label: cap(t),
|
|
109
|
+
color: TYPE_COLOR[t] ?? '#888888',
|
|
110
|
+
})
|
|
111
|
+
|
|
94
112
|
export async function detailLoader({
|
|
95
113
|
params,
|
|
96
114
|
req,
|
|
97
115
|
}: LoaderCtx): Promise<DetailData | NativeVerdict> {
|
|
98
116
|
const name = params?.name ?? ''
|
|
99
|
-
const
|
|
100
|
-
const empty = emptyDetail(name, mode)
|
|
117
|
+
const empty = emptyDetail(req, name)
|
|
101
118
|
|
|
102
119
|
const p = await fetchPokemon(name)
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// HTTP STATUS (404 instead of 200). They are complementary. See GAPS S9.
|
|
120
|
+
// Native loaders can `return notFound(data)` to render THIS route's own
|
|
121
|
+
// template with HTTP 404. The `notFound: true` flag on `empty` drives the
|
|
122
|
+
// template's 404-block branch; the sentinel sets the HTTP STATUS.
|
|
107
123
|
if (!p) return notFound(empty)
|
|
108
124
|
|
|
109
125
|
const species = await fetchSpecies(p.id)
|
|
110
|
-
//
|
|
111
|
-
//
|
|
112
|
-
// the design wanted to stream — is therefore loaded BLOCKING here in the
|
|
113
|
-
// loader. See GAPS S3.
|
|
126
|
+
// A native route renders in Rust with no React tree, so <Suspense> streaming
|
|
127
|
+
// is impossible — the evolution chain (the slow fetch) is loaded BLOCKING here.
|
|
114
128
|
const rawEvo = await fetchEvolution(species.evolutionUrl)
|
|
115
129
|
|
|
116
130
|
const primary = p.types[0] ?? 'normal'
|
|
117
|
-
const tint = TYPE_COLOR[primary] ?? '
|
|
131
|
+
const tint = TYPE_COLOR[primary] ?? '#888888'
|
|
118
132
|
|
|
119
133
|
const stats = p.stats.map((s) => {
|
|
120
134
|
const pct = Math.min(100, Math.round((s.base / 200) * 100))
|
|
135
|
+
const bucket = statBucket(s.base)
|
|
121
136
|
return {
|
|
122
137
|
label: STAT_LABEL[s.name] ?? s.name,
|
|
123
138
|
base: s.base,
|
|
124
|
-
// Bare percent — the template builds the declaration via the S1 style
|
|
125
|
-
// object: `style={{ width: st.barWidth }}` → `width:62%`.
|
|
126
139
|
barWidth: `${pct}%`,
|
|
127
|
-
|
|
140
|
+
barColor: BAR_COLOR[bucket] ?? '#0ea5e9',
|
|
128
141
|
}
|
|
129
142
|
})
|
|
130
143
|
|
|
131
144
|
const abilities = p.abilities.map((a) => ({
|
|
132
145
|
displayName: cap(a),
|
|
133
146
|
initial: a.charAt(0).toUpperCase(),
|
|
134
|
-
// Bare color value — template does `style={{ background: a.iconColor }}`.
|
|
135
147
|
iconColor: tint,
|
|
136
148
|
}))
|
|
137
149
|
|
|
138
150
|
const evolution = rawEvo.map((s, i) => ({
|
|
139
151
|
id: s.id,
|
|
140
|
-
name: s.name,
|
|
141
152
|
displayName: cap(s.name),
|
|
142
153
|
num: pad(s.id),
|
|
143
154
|
artwork: artwork(s.id),
|
|
144
155
|
detailHref: `/pokemon/${s.name}`,
|
|
145
156
|
levelLabel: s.minLevel != null ? `Lv ${s.minLevel}` : '',
|
|
146
|
-
// Real per-item conditionals now work in native routes (S11): the template
|
|
147
|
-
// tests these booleans instead of toggling a precomputed `dex-hide` class.
|
|
148
157
|
isFirst: i === 0,
|
|
149
158
|
showLevel: i > 0 && s.minLevel != null,
|
|
150
|
-
|
|
159
|
+
isCurrent: s.id === p.id,
|
|
151
160
|
}))
|
|
152
161
|
|
|
153
|
-
const hasEvolution = evolution.length > 1
|
|
154
|
-
|
|
155
162
|
return {
|
|
163
|
+
...chrome(req, `${cap(p.name)} · PokéDex`, cap(p.name)),
|
|
156
164
|
notFound: false,
|
|
157
|
-
// Chrome fields (ChromeData) read by AppLayout from the merged context.
|
|
158
|
-
title: `${cap(p.name)} · PokéDex`,
|
|
159
|
-
active: 'list',
|
|
160
|
-
crumb: cap(p.name),
|
|
161
|
-
mode,
|
|
162
165
|
name: p.name,
|
|
163
166
|
id: p.id,
|
|
164
167
|
displayName: cap(p.name),
|
|
@@ -169,17 +172,18 @@ export async function detailLoader({
|
|
|
169
172
|
heightLabel: `${(p.height / 10).toFixed(1)} m`,
|
|
170
173
|
weightLabel: `${(p.weight / 10).toFixed(1)} kg`,
|
|
171
174
|
abilityCount: p.abilities.length,
|
|
172
|
-
|
|
175
|
+
// Loaders are full JS — template literals are fine HERE (the constraint is
|
|
176
|
+
// on the native template body only). Brand-tinted hero gradient.
|
|
177
|
+
heroBg: `linear-gradient(160deg, ${tint}33, transparent 70%)`,
|
|
173
178
|
types: p.types.map(typeBadge),
|
|
174
179
|
stats,
|
|
175
180
|
statTotal: p.stats.reduce((a, s) => a + s.base, 0),
|
|
176
181
|
abilities,
|
|
177
182
|
hasAbilities: abilities.length > 0,
|
|
178
183
|
evolution,
|
|
179
|
-
hasEvolution,
|
|
184
|
+
hasEvolution: evolution.length > 1,
|
|
180
185
|
// Native templates can't call JSON.stringify, so precompute the x-props JSON
|
|
181
|
-
// here.
|
|
182
|
-
// the directive runtime JSON.parses it back into the behavior's `props`.
|
|
186
|
+
// here. AddToTeamButton's prop contract is unchanged.
|
|
183
187
|
addProps: JSON.stringify({
|
|
184
188
|
id: p.id,
|
|
185
189
|
name: p.name,
|
|
@@ -188,18 +192,13 @@ export async function detailLoader({
|
|
|
188
192
|
types: p.types,
|
|
189
193
|
artwork: p.artwork,
|
|
190
194
|
}),
|
|
191
|
-
teamProps: { teamInitial: teamStore.list() },
|
|
192
195
|
}
|
|
193
196
|
}
|
|
194
197
|
|
|
195
|
-
function emptyDetail(
|
|
198
|
+
function emptyDetail(req: BrustRequest, name: string): DetailData {
|
|
196
199
|
return {
|
|
200
|
+
...chrome(req, `${cap(name)} · PokéDex`, cap(name)),
|
|
197
201
|
notFound: true,
|
|
198
|
-
// Chrome fields (ChromeData) read by AppLayout from the merged context.
|
|
199
|
-
title: `${cap(name)} · PokéDex`,
|
|
200
|
-
active: 'list',
|
|
201
|
-
crumb: cap(name),
|
|
202
|
-
mode,
|
|
203
202
|
name,
|
|
204
203
|
id: 0,
|
|
205
204
|
displayName: cap(name),
|
|
@@ -226,7 +225,6 @@ function emptyDetail(name: string, mode: 'dark' | 'light'): DetailData {
|
|
|
226
225
|
types: [],
|
|
227
226
|
artwork: '',
|
|
228
227
|
}),
|
|
229
|
-
teamProps: { teamInitial: teamStore.list() },
|
|
230
228
|
}
|
|
231
229
|
}
|
|
232
230
|
|
|
@@ -251,45 +249,58 @@ const SHORT: Record<string, string> = {
|
|
|
251
249
|
fairy: 'FAI',
|
|
252
250
|
}
|
|
253
251
|
|
|
252
|
+
// Effectiveness → static Tailwind utility class string. These are literals in a
|
|
253
|
+
// .ts file scanned by `@source`, so the scanner sees every class. The header /
|
|
254
|
+
// row-head / corner / data cells share a base sizing class.
|
|
255
|
+
const CELL_BASE =
|
|
256
|
+
'flex items-center justify-center text-xs font-semibold tabular-nums aspect-square'
|
|
257
|
+
const HEAD_BASE =
|
|
258
|
+
'flex items-center justify-center text-[10px] font-bold uppercase tracking-tight text-white aspect-square'
|
|
259
|
+
const CELL_CLASS: Record<string, string> = {
|
|
260
|
+
super: `${CELL_BASE} bg-green-500/25 text-green-700 dark:text-green-300`,
|
|
261
|
+
weak: `${CELL_BASE} bg-red-500/20 text-red-700 dark:text-red-300`,
|
|
262
|
+
none: `${CELL_BASE} bg-slate-800/80 text-slate-200`,
|
|
263
|
+
normal: `${CELL_BASE} text-slate-300 dark:text-slate-600`,
|
|
264
|
+
}
|
|
265
|
+
|
|
254
266
|
export async function typeChartLoader({ req }: LoaderCtx): Promise<TypeChartData> {
|
|
255
|
-
// Fan out 18
|
|
256
|
-
//
|
|
267
|
+
// Fan out 18 type fetches; each goes through cachedFetch so duplicate in-flight
|
|
268
|
+
// GETs within the request dedupe.
|
|
257
269
|
const relations = await Promise.all(ALL_TYPES.map((t) => fetchTypeRelations(t)))
|
|
258
270
|
|
|
259
271
|
// Build the 19×19 grid as nested rows (header row + one row per attacking
|
|
260
|
-
// type). The native template renders it with nested `.map()
|
|
261
|
-
|
|
262
|
-
// keeps every cell a direct grid item, so the layout is unchanged).
|
|
263
|
-
const rows: TypeChartData['rows'] = []
|
|
272
|
+
// type). The native template renders it with nested `.map()`.
|
|
273
|
+
const rows: TypeChartRowVM[] = []
|
|
264
274
|
|
|
265
|
-
// Header row: corner + 18 defending-type column heads.
|
|
266
275
|
const headerCells: TypeChartCellVM[] = [
|
|
267
276
|
{
|
|
268
277
|
id: '0-0',
|
|
269
|
-
className:
|
|
270
|
-
content: 'ATK
|
|
271
|
-
title: 'Attacking
|
|
278
|
+
className: `${HEAD_BASE} sticky left-0 top-0 z-20 text-[9px]`,
|
|
279
|
+
content: 'ATK/DEF',
|
|
280
|
+
title: 'Attacking / Defending',
|
|
281
|
+
bg: '#334155', // slate-700
|
|
272
282
|
},
|
|
273
283
|
]
|
|
274
284
|
ALL_TYPES.forEach((def, j) => {
|
|
275
285
|
headerCells.push({
|
|
276
286
|
id: `0-${j + 1}`,
|
|
277
|
-
className:
|
|
287
|
+
className: `${HEAD_BASE} sticky top-0 z-10`,
|
|
278
288
|
content: SHORT[def] ?? def.slice(0, 3).toUpperCase(),
|
|
279
289
|
title: cap(def),
|
|
290
|
+
bg: TYPE_COLOR[def] ?? '#888888',
|
|
280
291
|
})
|
|
281
292
|
})
|
|
282
293
|
rows.push({ id: '0', cells: headerCells })
|
|
283
294
|
|
|
284
|
-
// One row per attacking type: row head + 18 effectiveness cells.
|
|
285
295
|
ALL_TYPES.forEach((atk, i) => {
|
|
286
|
-
const rel = relations[i]
|
|
296
|
+
const rel = relations[i] ?? {}
|
|
287
297
|
const rowCells: TypeChartCellVM[] = [
|
|
288
298
|
{
|
|
289
299
|
id: `${i + 1}-0`,
|
|
290
|
-
className:
|
|
300
|
+
className: `${HEAD_BASE} sticky left-0 z-10`,
|
|
291
301
|
content: SHORT[atk] ?? atk.slice(0, 3).toUpperCase(),
|
|
292
302
|
title: cap(atk),
|
|
303
|
+
bg: TYPE_COLOR[atk] ?? '#888888',
|
|
293
304
|
},
|
|
294
305
|
]
|
|
295
306
|
ALL_TYPES.forEach((def, j) => {
|
|
@@ -298,42 +309,41 @@ export async function typeChartLoader({ req }: LoaderCtx): Promise<TypeChartData
|
|
|
298
309
|
if (mult === 2)
|
|
299
310
|
rowCells.push({
|
|
300
311
|
id,
|
|
301
|
-
className:
|
|
312
|
+
className: CELL_CLASS.super!,
|
|
302
313
|
content: '2',
|
|
303
314
|
title: `${cap(atk)} → ${cap(def)}: 2× (super effective)`,
|
|
315
|
+
bg: '',
|
|
304
316
|
})
|
|
305
317
|
else if (mult === 0.5)
|
|
306
318
|
rowCells.push({
|
|
307
319
|
id,
|
|
308
|
-
className:
|
|
320
|
+
className: CELL_CLASS.weak!,
|
|
309
321
|
content: '½',
|
|
310
322
|
title: `${cap(atk)} → ${cap(def)}: ½× (not very effective)`,
|
|
323
|
+
bg: '',
|
|
311
324
|
})
|
|
312
325
|
else if (mult === 0)
|
|
313
326
|
rowCells.push({
|
|
314
327
|
id,
|
|
315
|
-
className:
|
|
328
|
+
className: CELL_CLASS.none!,
|
|
316
329
|
content: '0',
|
|
317
330
|
title: `${cap(atk)} → ${cap(def)}: 0× (no effect)`,
|
|
331
|
+
bg: '',
|
|
318
332
|
})
|
|
319
333
|
else
|
|
320
334
|
rowCells.push({
|
|
321
335
|
id,
|
|
322
|
-
className:
|
|
336
|
+
className: CELL_CLASS.normal!,
|
|
323
337
|
content: '',
|
|
324
338
|
title: `${cap(atk)} → ${cap(def)}: 1×`,
|
|
339
|
+
bg: '',
|
|
325
340
|
})
|
|
326
341
|
})
|
|
327
342
|
rows.push({ id: String(i + 1), cells: rowCells })
|
|
328
343
|
})
|
|
329
344
|
|
|
330
345
|
return {
|
|
346
|
+
...chrome(req, 'PokéDex · type chart', 'Type chart'),
|
|
331
347
|
rows,
|
|
332
|
-
// Chrome fields (ChromeData) read by AppLayout from the merged context.
|
|
333
|
-
title: 'PokéDex · type chart',
|
|
334
|
-
active: 'typechart',
|
|
335
|
-
crumb: 'Type chart',
|
|
336
|
-
teamProps: { teamInitial: teamStore.list() },
|
|
337
|
-
mode: req.cookies.mode === 'light' ? 'light' : 'dark',
|
|
338
348
|
}
|
|
339
349
|
}
|
|
@@ -20,28 +20,28 @@ export const cap = (s: string): string =>
|
|
|
20
20
|
|
|
21
21
|
export const pad = (n: number): string => `#${String(n).padStart(4, '0')}`
|
|
22
22
|
|
|
23
|
-
/** Pokémon type →
|
|
24
|
-
*
|
|
25
|
-
*
|
|
23
|
+
/** Pokémon type → real hex color. Tailwind's scanner can't see runtime-built
|
|
24
|
+
* class strings, so type tints are applied as inline `style` values from the
|
|
25
|
+
* loader using these hexes. */
|
|
26
26
|
export const TYPE_COLOR: Record<string, string> = {
|
|
27
|
-
normal: '
|
|
28
|
-
fire: '
|
|
29
|
-
water: '
|
|
30
|
-
grass: '
|
|
31
|
-
electric: '
|
|
32
|
-
ice: '
|
|
33
|
-
fighting: '
|
|
34
|
-
poison: '
|
|
35
|
-
ground: '
|
|
36
|
-
flying: '
|
|
37
|
-
psychic: '
|
|
38
|
-
bug: '
|
|
39
|
-
rock: '
|
|
40
|
-
ghost: '
|
|
41
|
-
dragon: '
|
|
42
|
-
dark: '
|
|
43
|
-
steel: '
|
|
44
|
-
fairy: '
|
|
27
|
+
normal: '#9099a1',
|
|
28
|
+
fire: '#ef7444',
|
|
29
|
+
water: '#4d90d5',
|
|
30
|
+
grass: '#63bb5b',
|
|
31
|
+
electric: '#f5c84b',
|
|
32
|
+
ice: '#74cec0',
|
|
33
|
+
fighting: '#ce4069',
|
|
34
|
+
poison: '#ab6ac8',
|
|
35
|
+
ground: '#d97746',
|
|
36
|
+
flying: '#8fa8dd',
|
|
37
|
+
psychic: '#f06fa0',
|
|
38
|
+
bug: '#90c12c',
|
|
39
|
+
rock: '#c7b78b',
|
|
40
|
+
ghost: '#5269ac',
|
|
41
|
+
dragon: '#0a6dc4',
|
|
42
|
+
dark: '#5a5366',
|
|
43
|
+
steel: '#5a8ea1',
|
|
44
|
+
fairy: '#ec8fe6',
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
export const STAT_LABEL: Record<string, string> = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// In-process team store — a module-scope Map that lives for the whole server
|
|
2
2
|
// process. It is GLOBAL across every request and every visitor: brust ships no
|
|
3
3
|
// per-session / request-context primitive, so for this dogfood the team is a
|
|
4
|
-
// single shared roster.
|
|
4
|
+
// single shared roster.
|
|
5
5
|
|
|
6
6
|
import type { TeamMember } from './types'
|
|
7
7
|
|