brustjs 0.1.27-alpha → 0.1.29-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 +108 -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 -116
- package/example/pokedex/lib/pokeapi.ts +21 -21
- package/example/pokedex/lib/team-store.ts +1 -1
- package/example/pokedex/lib/types.ts +72 -94
- package/example/pokedex/pages/BrowsePage.tsx +30 -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 +145 -31
- 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,78 @@ 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 {
|
|
70
|
-
|
|
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',
|
|
93
|
+
...chrome(req, 'Pokédex · Browse', 'Pokédex'),
|
|
94
|
+
dexProps: JSON.stringify({ items }),
|
|
91
95
|
}
|
|
92
96
|
}
|
|
93
97
|
|
|
98
|
+
/** Base-stat bucket → bar fill hex. Bucket order: hi / mid / low / min. */
|
|
99
|
+
const BAR_COLOR: Record<string, string> = {
|
|
100
|
+
hi: '#16a34a', // green-600
|
|
101
|
+
mid: '#0ea5e9', // sky-500
|
|
102
|
+
low: '#f59e0b', // amber-500
|
|
103
|
+
min: '#ef4444', // red-500
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const typeBadge = (t: string): TypeBadgeVM => ({
|
|
107
|
+
label: cap(t),
|
|
108
|
+
color: TYPE_COLOR[t] ?? '#888888',
|
|
109
|
+
})
|
|
110
|
+
|
|
94
111
|
export async function detailLoader({
|
|
95
112
|
params,
|
|
96
113
|
req,
|
|
97
114
|
}: LoaderCtx): Promise<DetailData | NativeVerdict> {
|
|
98
115
|
const name = params?.name ?? ''
|
|
99
|
-
const
|
|
100
|
-
const empty = emptyDetail(name, mode)
|
|
116
|
+
const empty = emptyDetail(req, name)
|
|
101
117
|
|
|
102
118
|
const p = await fetchPokemon(name)
|
|
103
|
-
//
|
|
104
|
-
//
|
|
105
|
-
//
|
|
106
|
-
// HTTP STATUS (404 instead of 200). They are complementary. See GAPS S9.
|
|
119
|
+
// Native loaders can `return notFound(data)` to render THIS route's own
|
|
120
|
+
// template with HTTP 404. The `notFound: true` flag on `empty` drives the
|
|
121
|
+
// template's 404-block branch; the sentinel sets the HTTP STATUS.
|
|
107
122
|
if (!p) return notFound(empty)
|
|
108
123
|
|
|
109
124
|
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.
|
|
125
|
+
// A native route renders in Rust with no React tree, so <Suspense> streaming
|
|
126
|
+
// is impossible — the evolution chain (the slow fetch) is loaded BLOCKING here.
|
|
114
127
|
const rawEvo = await fetchEvolution(species.evolutionUrl)
|
|
115
128
|
|
|
116
129
|
const primary = p.types[0] ?? 'normal'
|
|
117
|
-
const tint = TYPE_COLOR[primary] ?? '
|
|
130
|
+
const tint = TYPE_COLOR[primary] ?? '#888888'
|
|
118
131
|
|
|
119
132
|
const stats = p.stats.map((s) => {
|
|
120
133
|
const pct = Math.min(100, Math.round((s.base / 200) * 100))
|
|
134
|
+
const bucket = statBucket(s.base)
|
|
121
135
|
return {
|
|
122
136
|
label: STAT_LABEL[s.name] ?? s.name,
|
|
123
137
|
base: s.base,
|
|
124
|
-
// Bare percent — the template builds the declaration via the S1 style
|
|
125
|
-
// object: `style={{ width: st.barWidth }}` → `width:62%`.
|
|
126
138
|
barWidth: `${pct}%`,
|
|
127
|
-
|
|
139
|
+
barColor: BAR_COLOR[bucket] ?? '#0ea5e9',
|
|
128
140
|
}
|
|
129
141
|
})
|
|
130
142
|
|
|
131
143
|
const abilities = p.abilities.map((a) => ({
|
|
132
144
|
displayName: cap(a),
|
|
133
145
|
initial: a.charAt(0).toUpperCase(),
|
|
134
|
-
// Bare color value — template does `style={{ background: a.iconColor }}`.
|
|
135
146
|
iconColor: tint,
|
|
136
147
|
}))
|
|
137
148
|
|
|
138
149
|
const evolution = rawEvo.map((s, i) => ({
|
|
139
150
|
id: s.id,
|
|
140
|
-
name: s.name,
|
|
141
151
|
displayName: cap(s.name),
|
|
142
152
|
num: pad(s.id),
|
|
143
153
|
artwork: artwork(s.id),
|
|
144
154
|
detailHref: `/pokemon/${s.name}`,
|
|
145
155
|
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
156
|
isFirst: i === 0,
|
|
149
157
|
showLevel: i > 0 && s.minLevel != null,
|
|
150
|
-
|
|
158
|
+
isCurrent: s.id === p.id,
|
|
151
159
|
}))
|
|
152
160
|
|
|
153
|
-
const hasEvolution = evolution.length > 1
|
|
154
|
-
|
|
155
161
|
return {
|
|
162
|
+
...chrome(req, `${cap(p.name)} · PokéDex`, cap(p.name)),
|
|
156
163
|
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
164
|
name: p.name,
|
|
163
165
|
id: p.id,
|
|
164
166
|
displayName: cap(p.name),
|
|
@@ -169,17 +171,18 @@ export async function detailLoader({
|
|
|
169
171
|
heightLabel: `${(p.height / 10).toFixed(1)} m`,
|
|
170
172
|
weightLabel: `${(p.weight / 10).toFixed(1)} kg`,
|
|
171
173
|
abilityCount: p.abilities.length,
|
|
172
|
-
|
|
174
|
+
// Loaders are full JS — template literals are fine HERE (the constraint is
|
|
175
|
+
// on the native template body only). Brand-tinted hero gradient.
|
|
176
|
+
heroBg: `linear-gradient(160deg, ${tint}33, transparent 70%)`,
|
|
173
177
|
types: p.types.map(typeBadge),
|
|
174
178
|
stats,
|
|
175
179
|
statTotal: p.stats.reduce((a, s) => a + s.base, 0),
|
|
176
180
|
abilities,
|
|
177
181
|
hasAbilities: abilities.length > 0,
|
|
178
182
|
evolution,
|
|
179
|
-
hasEvolution,
|
|
183
|
+
hasEvolution: evolution.length > 1,
|
|
180
184
|
// 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`.
|
|
185
|
+
// here. AddToTeamButton's prop contract is unchanged.
|
|
183
186
|
addProps: JSON.stringify({
|
|
184
187
|
id: p.id,
|
|
185
188
|
name: p.name,
|
|
@@ -188,18 +191,13 @@ export async function detailLoader({
|
|
|
188
191
|
types: p.types,
|
|
189
192
|
artwork: p.artwork,
|
|
190
193
|
}),
|
|
191
|
-
teamProps: { teamInitial: teamStore.list() },
|
|
192
194
|
}
|
|
193
195
|
}
|
|
194
196
|
|
|
195
|
-
function emptyDetail(
|
|
197
|
+
function emptyDetail(req: BrustRequest, name: string): DetailData {
|
|
196
198
|
return {
|
|
199
|
+
...chrome(req, `${cap(name)} · PokéDex`, cap(name)),
|
|
197
200
|
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
201
|
name,
|
|
204
202
|
id: 0,
|
|
205
203
|
displayName: cap(name),
|
|
@@ -226,7 +224,6 @@ function emptyDetail(name: string, mode: 'dark' | 'light'): DetailData {
|
|
|
226
224
|
types: [],
|
|
227
225
|
artwork: '',
|
|
228
226
|
}),
|
|
229
|
-
teamProps: { teamInitial: teamStore.list() },
|
|
230
227
|
}
|
|
231
228
|
}
|
|
232
229
|
|
|
@@ -251,45 +248,58 @@ const SHORT: Record<string, string> = {
|
|
|
251
248
|
fairy: 'FAI',
|
|
252
249
|
}
|
|
253
250
|
|
|
251
|
+
// Effectiveness → static Tailwind utility class string. These are literals in a
|
|
252
|
+
// .ts file scanned by `@source`, so the scanner sees every class. The header /
|
|
253
|
+
// row-head / corner / data cells share a base sizing class.
|
|
254
|
+
const CELL_BASE =
|
|
255
|
+
'flex items-center justify-center text-xs font-semibold tabular-nums aspect-square'
|
|
256
|
+
const HEAD_BASE =
|
|
257
|
+
'flex items-center justify-center text-[10px] font-bold uppercase tracking-tight text-white aspect-square'
|
|
258
|
+
const CELL_CLASS: Record<string, string> = {
|
|
259
|
+
super: `${CELL_BASE} bg-green-500/25 text-green-700 dark:text-green-300`,
|
|
260
|
+
weak: `${CELL_BASE} bg-red-500/20 text-red-700 dark:text-red-300`,
|
|
261
|
+
none: `${CELL_BASE} bg-slate-800/80 text-slate-200`,
|
|
262
|
+
normal: `${CELL_BASE} text-slate-300 dark:text-slate-600`,
|
|
263
|
+
}
|
|
264
|
+
|
|
254
265
|
export async function typeChartLoader({ req }: LoaderCtx): Promise<TypeChartData> {
|
|
255
|
-
// Fan out 18
|
|
256
|
-
//
|
|
266
|
+
// Fan out 18 type fetches; each goes through cachedFetch so duplicate in-flight
|
|
267
|
+
// GETs within the request dedupe.
|
|
257
268
|
const relations = await Promise.all(ALL_TYPES.map((t) => fetchTypeRelations(t)))
|
|
258
269
|
|
|
259
270
|
// 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'] = []
|
|
271
|
+
// type). The native template renders it with nested `.map()`.
|
|
272
|
+
const rows: TypeChartRowVM[] = []
|
|
264
273
|
|
|
265
|
-
// Header row: corner + 18 defending-type column heads.
|
|
266
274
|
const headerCells: TypeChartCellVM[] = [
|
|
267
275
|
{
|
|
268
276
|
id: '0-0',
|
|
269
|
-
className:
|
|
270
|
-
content: 'ATK
|
|
271
|
-
title: 'Attacking
|
|
277
|
+
className: `${HEAD_BASE} sticky left-0 top-0 z-20 text-[9px]`,
|
|
278
|
+
content: 'ATK/DEF',
|
|
279
|
+
title: 'Attacking / Defending',
|
|
280
|
+
bg: '#334155', // slate-700
|
|
272
281
|
},
|
|
273
282
|
]
|
|
274
283
|
ALL_TYPES.forEach((def, j) => {
|
|
275
284
|
headerCells.push({
|
|
276
285
|
id: `0-${j + 1}`,
|
|
277
|
-
className:
|
|
286
|
+
className: `${HEAD_BASE} sticky top-0 z-10`,
|
|
278
287
|
content: SHORT[def] ?? def.slice(0, 3).toUpperCase(),
|
|
279
288
|
title: cap(def),
|
|
289
|
+
bg: TYPE_COLOR[def] ?? '#888888',
|
|
280
290
|
})
|
|
281
291
|
})
|
|
282
292
|
rows.push({ id: '0', cells: headerCells })
|
|
283
293
|
|
|
284
|
-
// One row per attacking type: row head + 18 effectiveness cells.
|
|
285
294
|
ALL_TYPES.forEach((atk, i) => {
|
|
286
|
-
const rel = relations[i]
|
|
295
|
+
const rel = relations[i] ?? {}
|
|
287
296
|
const rowCells: TypeChartCellVM[] = [
|
|
288
297
|
{
|
|
289
298
|
id: `${i + 1}-0`,
|
|
290
|
-
className:
|
|
299
|
+
className: `${HEAD_BASE} sticky left-0 z-10`,
|
|
291
300
|
content: SHORT[atk] ?? atk.slice(0, 3).toUpperCase(),
|
|
292
301
|
title: cap(atk),
|
|
302
|
+
bg: TYPE_COLOR[atk] ?? '#888888',
|
|
293
303
|
},
|
|
294
304
|
]
|
|
295
305
|
ALL_TYPES.forEach((def, j) => {
|
|
@@ -298,42 +308,41 @@ export async function typeChartLoader({ req }: LoaderCtx): Promise<TypeChartData
|
|
|
298
308
|
if (mult === 2)
|
|
299
309
|
rowCells.push({
|
|
300
310
|
id,
|
|
301
|
-
className:
|
|
311
|
+
className: CELL_CLASS.super!,
|
|
302
312
|
content: '2',
|
|
303
313
|
title: `${cap(atk)} → ${cap(def)}: 2× (super effective)`,
|
|
314
|
+
bg: '',
|
|
304
315
|
})
|
|
305
316
|
else if (mult === 0.5)
|
|
306
317
|
rowCells.push({
|
|
307
318
|
id,
|
|
308
|
-
className:
|
|
319
|
+
className: CELL_CLASS.weak!,
|
|
309
320
|
content: '½',
|
|
310
321
|
title: `${cap(atk)} → ${cap(def)}: ½× (not very effective)`,
|
|
322
|
+
bg: '',
|
|
311
323
|
})
|
|
312
324
|
else if (mult === 0)
|
|
313
325
|
rowCells.push({
|
|
314
326
|
id,
|
|
315
|
-
className:
|
|
327
|
+
className: CELL_CLASS.none!,
|
|
316
328
|
content: '0',
|
|
317
329
|
title: `${cap(atk)} → ${cap(def)}: 0× (no effect)`,
|
|
330
|
+
bg: '',
|
|
318
331
|
})
|
|
319
332
|
else
|
|
320
333
|
rowCells.push({
|
|
321
334
|
id,
|
|
322
|
-
className:
|
|
335
|
+
className: CELL_CLASS.normal!,
|
|
323
336
|
content: '',
|
|
324
337
|
title: `${cap(atk)} → ${cap(def)}: 1×`,
|
|
338
|
+
bg: '',
|
|
325
339
|
})
|
|
326
340
|
})
|
|
327
341
|
rows.push({ id: String(i + 1), cells: rowCells })
|
|
328
342
|
})
|
|
329
343
|
|
|
330
344
|
return {
|
|
345
|
+
...chrome(req, 'PokéDex · type chart', 'Type chart'),
|
|
331
346
|
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
347
|
}
|
|
339
348
|
}
|
|
@@ -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
|
|