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,29 +1,34 @@
|
|
|
1
1
|
// Domain + view-model types for the PokéDex example.
|
|
2
2
|
//
|
|
3
3
|
// NATIVE NOTE: the page components are `native: true` routes compiled to
|
|
4
|
-
// minijinja.
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
// minijinja. Loaders precompute formatted strings, classNames, hrefs, inline
|
|
5
|
+
// style values, and x-props JSON; the templates only interpolate member-paths,
|
|
6
|
+
// run `.map()`, and branch on inline conditionals.
|
|
7
|
+
|
|
8
|
+
/** In-process / island team store member. */
|
|
9
|
+
export interface TeamMember {
|
|
10
|
+
id: number
|
|
11
|
+
name: string
|
|
12
|
+
displayName: string
|
|
13
|
+
types: string[]
|
|
14
|
+
artwork: string
|
|
15
|
+
num: string
|
|
16
|
+
addedAt: number
|
|
17
|
+
}
|
|
9
18
|
|
|
10
19
|
/** Chrome view-model the router-level AppLayout reads from the MERGED loader
|
|
11
|
-
* context
|
|
12
|
-
*
|
|
13
|
-
* fields and the chain-loader merge folds them into the one flat jinja context
|
|
14
|
-
* AppLayout's template reads ({{ title }}, active === 'list', teamProps). The
|
|
15
|
-
* key names are chosen NOT to collide with any page-data field. */
|
|
20
|
+
* context. Each leaf loader returns these fields; the chain-loader merge folds
|
|
21
|
+
* them into the one flat jinja context AppLayout's template reads. */
|
|
16
22
|
export interface ChromeData {
|
|
17
23
|
title: string // dynamic <title> + nav header, via AppLayout's <BrustPage title={title}>
|
|
18
|
-
active: 'list' | 'typechart' // which sidebar nav item gets is-active (S11 conditional)
|
|
19
24
|
crumb: string // topbar breadcrumb leaf label
|
|
20
|
-
teamProps: { teamInitial: TeamMember[] } // floating team-dock island initial state
|
|
21
25
|
mode: 'dark' | 'light' // theme, read from the `mode` cookie → <html data-mode={mode}>
|
|
26
|
+
teamProps: { teamInitial: TeamMember[] } // floating team-dock island initial state
|
|
22
27
|
}
|
|
23
28
|
|
|
24
|
-
/** A single
|
|
25
|
-
*
|
|
26
|
-
export interface
|
|
29
|
+
/** A single dex grid cell — derived from the list endpoint alone (no detail
|
|
30
|
+
* fetch). */
|
|
31
|
+
export interface DexCard {
|
|
27
32
|
id: number
|
|
28
33
|
name: string
|
|
29
34
|
displayName: string // "Bulbasaur"
|
|
@@ -32,127 +37,100 @@ export interface CardVM {
|
|
|
32
37
|
detailHref: string // "/pokemon/bulbasaur"
|
|
33
38
|
}
|
|
34
39
|
|
|
35
|
-
export interface ListData extends ChromeData {
|
|
36
|
-
items: CardVM[]
|
|
37
|
-
total: number
|
|
38
|
-
totalLabel: string // "1,302"
|
|
39
|
-
offset: number
|
|
40
|
-
showingLabel: string // "1–20 of 1,302"
|
|
41
|
-
pageLabel: string // "1 / 66"
|
|
42
|
-
// Native routes now support conditionals (GAPS S11 closed): the template
|
|
43
|
-
// branches with `{flags.hasPrev ? <a/> : <span/>}` on these booleans.
|
|
44
|
-
hasPrev: boolean
|
|
45
|
-
hasNext: boolean
|
|
46
|
-
prevHref: string
|
|
47
|
-
nextHref: string
|
|
48
|
-
offsetLabel: string // raw offset for the loader-echo line
|
|
49
|
-
}
|
|
50
|
-
|
|
51
40
|
export interface TypeBadgeVM {
|
|
52
41
|
label: string // "Grass"
|
|
53
|
-
|
|
42
|
+
color: string // hex tint — fed into an inline style value
|
|
54
43
|
}
|
|
55
44
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
45
|
+
/** One base-stat bar row on the detail page. All formatting precomputed. */
|
|
46
|
+
export interface StatBarVM {
|
|
47
|
+
label: string // "HP" / "Atk" …
|
|
48
|
+
base: number // raw base value
|
|
49
|
+
barWidth: string // "62%" — fed into style={{ width }}
|
|
50
|
+
barColor: string // hex per bucket — fed into style={{ background }}
|
|
61
51
|
}
|
|
62
52
|
|
|
53
|
+
/** One ability chip on the detail page. */
|
|
63
54
|
export interface AbilityVM {
|
|
64
55
|
displayName: string // "Overgrow"
|
|
65
56
|
initial: string // "O"
|
|
66
|
-
iconColor: string //
|
|
57
|
+
iconColor: string // hex tint — style={{ background }}
|
|
67
58
|
}
|
|
68
59
|
|
|
69
|
-
|
|
60
|
+
/** One stage of the evolution chain. */
|
|
61
|
+
export interface EvoStageVM {
|
|
70
62
|
id: number
|
|
71
|
-
name: string
|
|
72
63
|
displayName: string
|
|
73
|
-
num: string
|
|
64
|
+
num: string // "#0001"
|
|
74
65
|
artwork: string
|
|
75
|
-
detailHref: string
|
|
76
|
-
levelLabel: string // "Lv 16"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
66
|
+
detailHref: string // "/pokemon/ivysaur"
|
|
67
|
+
levelLabel: string // "Lv 16" or ""
|
|
68
|
+
isFirst: boolean
|
|
69
|
+
showLevel: boolean
|
|
70
|
+
isCurrent: boolean
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** One "browse by type" tile on the home page. */
|
|
74
|
+
export interface TypeTileVM {
|
|
75
|
+
name: string // raw type key, used as the .map() key
|
|
76
|
+
label: string // "Grass"
|
|
77
|
+
color: string // hex tint — fed into an inline style value
|
|
78
|
+
href: string // "/pokedex"
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Home landing page data — curated featured strip + type tiles + chrome. */
|
|
82
|
+
export interface HomeData extends ChromeData {
|
|
83
|
+
featured: DexCard[]
|
|
84
|
+
typeTiles: TypeTileVM[]
|
|
83
85
|
}
|
|
84
86
|
|
|
85
|
-
/**
|
|
87
|
+
/** Browse (dex grid) page data. `dexProps` is the loader-precomputed JSON
|
|
88
|
+
* string handed to the DexFilter native directive (gen-1 grid, keyed x-for). */
|
|
89
|
+
export interface BrowseData extends ChromeData {
|
|
90
|
+
dexProps: string
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Detail page data. Every formatted string / className / inline-style value /
|
|
94
|
+
* x-props JSON is precomputed here so the native template only interpolates. */
|
|
86
95
|
export interface DetailData extends ChromeData {
|
|
87
96
|
notFound: boolean
|
|
88
|
-
// Native routes now branch with `{notFound ? <NotFound/> : <Content/>}` (S11),
|
|
89
|
-
// so the content and 404 block are mutually exclusive at render time rather
|
|
90
|
-
// than both emitted with one hidden via a precomputed class.
|
|
91
|
-
// The dynamic <title> is `title` (ChromeData), read by AppLayout's
|
|
92
|
-
// <BrustPage title={title}> — no separate pageTitle field.
|
|
93
97
|
name: string
|
|
94
|
-
// present only when notFound === false:
|
|
95
98
|
id: number
|
|
96
99
|
displayName: string
|
|
97
|
-
num: string
|
|
100
|
+
num: string // "#0001"
|
|
98
101
|
artwork: string
|
|
99
|
-
genus: string
|
|
102
|
+
genus: string // "Seed Pokémon"
|
|
100
103
|
flavorText: string
|
|
101
104
|
heightLabel: string // "0.7 m"
|
|
102
105
|
weightLabel: string // "6.9 kg"
|
|
103
106
|
abilityCount: number
|
|
104
|
-
heroBg: string // gradient
|
|
107
|
+
heroBg: string // CSS gradient string built in the loader from the type tint
|
|
105
108
|
types: TypeBadgeVM[]
|
|
106
|
-
stats:
|
|
109
|
+
stats: StatBarVM[]
|
|
107
110
|
statTotal: number
|
|
108
111
|
abilities: AbilityVM[]
|
|
109
112
|
hasAbilities: boolean
|
|
110
|
-
evolution:
|
|
113
|
+
evolution: EvoStageVM[]
|
|
111
114
|
hasEvolution: boolean
|
|
112
|
-
|
|
113
|
-
// object literals). addProps is the loader-precomputed JSON string handed to
|
|
114
|
-
// <AddToTeamButton data={addProps} /> → x-props (Spec B native directives).
|
|
115
|
-
addProps: string
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/** Shape of the AddToTeamButton native behavior's `props` (JSON-parsed from
|
|
119
|
-
* x-props). Matches the action body fields so toggle() can post it directly. */
|
|
120
|
-
export interface AddToTeamProps {
|
|
121
|
-
id: number
|
|
122
|
-
name: string
|
|
123
|
-
displayName: string
|
|
124
|
-
num: string
|
|
125
|
-
types: string[]
|
|
126
|
-
artwork: string
|
|
115
|
+
addProps: string // loader-precomputed x-props JSON for AddToTeamButton
|
|
127
116
|
}
|
|
128
117
|
|
|
129
118
|
/** One cell of the type chart. */
|
|
130
119
|
export interface TypeChartCellVM {
|
|
131
120
|
id: string // stable key "row-col"
|
|
132
|
-
className: string //
|
|
121
|
+
className: string // static Tailwind utility string the loader picks per effectiveness
|
|
133
122
|
content: string // "2", "½", "0", a type short-code, or ""
|
|
134
123
|
title: string // tooltip
|
|
124
|
+
bg: string // inline background — type hex for header/row-head cells, '' for data cells
|
|
135
125
|
}
|
|
136
126
|
|
|
137
|
-
/** One row of the type chart
|
|
138
|
-
* native template renders rows.map(r => r.cells.map(c => …)) — nested `.map()`
|
|
139
|
-
* is supported on the native path. */
|
|
127
|
+
/** One row (header or attack) of the type chart — nested cells. */
|
|
140
128
|
export interface TypeChartRowVM {
|
|
141
|
-
id: string
|
|
142
|
-
cells: TypeChartCellVM[]
|
|
129
|
+
id: string
|
|
130
|
+
cells: TypeChartCellVM[]
|
|
143
131
|
}
|
|
144
132
|
|
|
133
|
+
/** Type chart page data — nested rows[].cells[] grid, rendered with nested .map(). */
|
|
145
134
|
export interface TypeChartData extends ChromeData {
|
|
146
|
-
rows: TypeChartRowVM[]
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
/** In-process team store member. */
|
|
150
|
-
export interface TeamMember {
|
|
151
|
-
id: number
|
|
152
|
-
name: string
|
|
153
|
-
displayName: string
|
|
154
|
-
types: string[]
|
|
155
|
-
artwork: string
|
|
156
|
-
num: string
|
|
157
|
-
addedAt: number
|
|
135
|
+
rows: TypeChartRowVM[]
|
|
158
136
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Native leaf route "/pokedex" — the dex grid. Renders into AppLayout's
|
|
2
|
+
// <Outlet/> slot (chrome lives in AppLayout). SINGLE return, NO local bindings.
|
|
3
|
+
//
|
|
4
|
+
// The grid itself is the DexFilter native directive: the loader precomputes the
|
|
5
|
+
// full gen-1 item list as the `dexProps` JSON string, handed to DexFilter via
|
|
6
|
+
// the native `data` prop → emitted as x-props. DexFilter owns search/sort and
|
|
7
|
+
// the keyed x-for reconcile.
|
|
8
|
+
import DexFilter from '../components/DexFilter'
|
|
9
|
+
import type { BrowseData } from '../lib/types'
|
|
10
|
+
|
|
11
|
+
export default function BrowsePage({ dexProps }: BrowseData) {
|
|
12
|
+
return (
|
|
13
|
+
<section className="py-2">
|
|
14
|
+
<div className="mb-6">
|
|
15
|
+
<h1 className="text-3xl font-extrabold tracking-tight text-slate-900 dark:text-white">
|
|
16
|
+
Pokédex
|
|
17
|
+
</h1>
|
|
18
|
+
<p className="mt-2 max-w-2xl text-slate-500 dark:text-slate-400">
|
|
19
|
+
The original 151. Search by name and toggle sort order — the grid reconciles live with a
|
|
20
|
+
keyed{' '}
|
|
21
|
+
<code className="rounded bg-slate-100 px-1.5 py-0.5 text-sm dark:bg-slate-800">
|
|
22
|
+
x-for
|
|
23
|
+
</code>
|
|
24
|
+
, no full re-render.
|
|
25
|
+
</p>
|
|
26
|
+
</div>
|
|
27
|
+
<DexFilter native data={dexProps} />
|
|
28
|
+
</section>
|
|
29
|
+
)
|
|
30
|
+
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
//
|
|
2
|
-
// <Outlet/> slot (chrome lives in AppLayout).
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
// native/jinja route has no React tree, so <Suspense> streaming is impossible —
|
|
6
|
-
// see ../FRAMEWORK-GAPS.md S3).
|
|
1
|
+
// Native leaf route "/pokemon/{name}" — the detail page. Renders into
|
|
2
|
+
// AppLayout's <Outlet/> slot (chrome lives in AppLayout). SINGLE return, NO
|
|
3
|
+
// local bindings — every formatted string / className / inline-style value /
|
|
4
|
+
// x-props JSON is precomputed in detailLoader.
|
|
7
5
|
//
|
|
8
|
-
// Native
|
|
9
|
-
// `{
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
|
|
6
|
+
// Native constraints: member-paths, `.map()`, inline conditionals
|
|
7
|
+
// (`{c ? a : b}` / `{c && x}`), and `style={{…}}` with member-path/literal
|
|
8
|
+
// values only. Type-tint colors and the hero gradient come from the loader as
|
|
9
|
+
// inline-style values (Tailwind can't scan runtime-built color classes). The
|
|
10
|
+
// evolution chain is loaded BLOCKING in the loader (native routes have no React
|
|
11
|
+
// tree, so <Suspense> streaming is impossible).
|
|
12
|
+
import { ArrowLeft, ChevronRight, Ruler, SearchX, Sigma, Weight } from 'lucide-react'
|
|
15
13
|
import AddToTeamButton from '../components/AddToTeamButton'
|
|
14
|
+
import Breadcrumb from '../components/Breadcrumb'
|
|
16
15
|
import type { DetailData } from '../lib/types'
|
|
17
16
|
|
|
18
17
|
export default function DetailPage({
|
|
@@ -36,65 +35,109 @@ export default function DetailPage({
|
|
|
36
35
|
addProps,
|
|
37
36
|
}: DetailData) {
|
|
38
37
|
return (
|
|
39
|
-
|
|
40
|
-
<
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
38
|
+
<section className="py-2">
|
|
39
|
+
<div className="mx-auto flex max-w-6xl items-center gap-1.5 px-4 pb-2 text-xs text-slate-400">
|
|
40
|
+
<a href="/" className="no-underline hover:text-slate-600 dark:hover:text-slate-200">
|
|
41
|
+
PokéDex
|
|
42
|
+
</a>
|
|
43
|
+
<ChevronRight size={14} isr={{ key: 'LcIconChevronRight14' }} />
|
|
44
|
+
<Breadcrumb native crumb={displayName} />
|
|
45
|
+
</div>
|
|
44
46
|
{notFound ? (
|
|
45
|
-
<div className="
|
|
46
|
-
<
|
|
47
|
-
<
|
|
48
|
-
<
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
<div className="mx-auto max-w-md rounded-3xl border border-slate-200 bg-white px-6 py-16 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
|
48
|
+
<SearchX size={48} className="mx-auto text-brand-500" isr={{ key: 'LcIconSearchX48' }} />
|
|
49
|
+
<div className="mt-3 text-6xl font-black tracking-tighter text-brand-500">404</div>
|
|
50
|
+
<h1 className="mt-4 text-2xl font-extrabold tracking-tight text-slate-900 dark:text-white">
|
|
51
|
+
No Pokémon named “{displayName}”
|
|
52
|
+
</h1>
|
|
53
|
+
<p className="mt-2 text-slate-500 dark:text-slate-400">
|
|
54
|
+
The Pokédex has no entry under that name. Check the spelling or browse the full list.
|
|
51
55
|
</p>
|
|
52
|
-
<a
|
|
53
|
-
|
|
56
|
+
<a
|
|
57
|
+
href="/pokedex"
|
|
58
|
+
className="mt-6 inline-flex 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"
|
|
59
|
+
>
|
|
60
|
+
<ArrowLeft size={16} isr={{ key: 'LcIconArrowLeft' }} />
|
|
61
|
+
Back to Pokédex
|
|
54
62
|
</a>
|
|
55
63
|
</div>
|
|
56
64
|
) : (
|
|
57
|
-
|
|
58
|
-
<div className="
|
|
59
|
-
<div
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
<div className="
|
|
64
|
-
|
|
65
|
+
<div className="space-y-10">
|
|
66
|
+
<div className="grid grid-cols-1 gap-6 lg:grid-cols-5">
|
|
67
|
+
<div
|
|
68
|
+
className="relative flex flex-col items-center overflow-hidden rounded-3xl border border-slate-200 bg-white p-6 text-center shadow-sm dark:border-slate-800 dark:bg-slate-900 lg:col-span-2"
|
|
69
|
+
style={{ background: heroBg }}
|
|
70
|
+
>
|
|
71
|
+
<div className="self-start text-sm font-bold tabular-nums text-slate-400">{num}</div>
|
|
72
|
+
<img
|
|
73
|
+
src={artwork}
|
|
74
|
+
alt={displayName}
|
|
75
|
+
className="h-56 w-56 object-contain drop-shadow-xl"
|
|
76
|
+
/>
|
|
77
|
+
<h1 className="mt-2 text-3xl font-extrabold tracking-tight text-slate-900 dark:text-white">
|
|
78
|
+
{displayName}
|
|
79
|
+
</h1>
|
|
80
|
+
<div className="mt-1 text-sm font-medium text-slate-500 dark:text-slate-400">
|
|
81
|
+
{genus}
|
|
65
82
|
</div>
|
|
66
|
-
<div className="
|
|
83
|
+
<div className="mt-4 flex flex-wrap items-center justify-center gap-2">
|
|
67
84
|
{types.map((t) => (
|
|
68
|
-
<span
|
|
85
|
+
<span
|
|
86
|
+
key={t.label}
|
|
87
|
+
style={{ background: t.color }}
|
|
88
|
+
className="rounded-full px-3 py-1 text-xs font-semibold uppercase tracking-wide text-white shadow-sm"
|
|
89
|
+
>
|
|
69
90
|
{t.label}
|
|
70
91
|
</span>
|
|
71
92
|
))}
|
|
72
93
|
</div>
|
|
73
|
-
<
|
|
94
|
+
<div className="mt-6 w-full max-w-xs">
|
|
95
|
+
<AddToTeamButton native data={addProps} />
|
|
96
|
+
</div>
|
|
74
97
|
</div>
|
|
75
98
|
|
|
76
|
-
<div className="
|
|
77
|
-
<div className="
|
|
78
|
-
<p className="
|
|
79
|
-
<div className="
|
|
80
|
-
<div className="
|
|
81
|
-
<div className="
|
|
82
|
-
|
|
99
|
+
<div className="space-y-6 lg:col-span-3">
|
|
100
|
+
<div className="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
|
101
|
+
<p className="text-slate-600 dark:text-slate-300">{flavorText}</p>
|
|
102
|
+
<div className="mt-5 grid grid-cols-3 gap-3">
|
|
103
|
+
<div className="rounded-2xl bg-slate-50 px-3 py-3 text-center dark:bg-slate-800/60">
|
|
104
|
+
<div className="text-lg font-extrabold text-slate-900 dark:text-white">
|
|
105
|
+
{heightLabel}
|
|
106
|
+
</div>
|
|
107
|
+
<div className="flex items-center justify-center gap-1 text-xs font-medium uppercase tracking-wide text-slate-400">
|
|
108
|
+
<Ruler size={12} isr={{ key: 'LcIconRuler12' }} />
|
|
109
|
+
Height
|
|
110
|
+
</div>
|
|
83
111
|
</div>
|
|
84
|
-
<div className="
|
|
85
|
-
<div className="
|
|
86
|
-
|
|
112
|
+
<div className="rounded-2xl bg-slate-50 px-3 py-3 text-center dark:bg-slate-800/60">
|
|
113
|
+
<div className="text-lg font-extrabold text-slate-900 dark:text-white">
|
|
114
|
+
{weightLabel}
|
|
115
|
+
</div>
|
|
116
|
+
<div className="flex items-center justify-center gap-1 text-xs font-medium uppercase tracking-wide text-slate-400">
|
|
117
|
+
<Weight size={12} isr={{ key: 'LcIconWeight12' }} />
|
|
118
|
+
Weight
|
|
119
|
+
</div>
|
|
87
120
|
</div>
|
|
88
|
-
<div className="
|
|
89
|
-
<div className="
|
|
90
|
-
|
|
121
|
+
<div className="rounded-2xl bg-slate-50 px-3 py-3 text-center dark:bg-slate-800/60">
|
|
122
|
+
<div className="text-lg font-extrabold text-slate-900 dark:text-white">
|
|
123
|
+
{abilityCount}
|
|
124
|
+
</div>
|
|
125
|
+
<div className="text-xs font-medium uppercase tracking-wide text-slate-400">
|
|
126
|
+
Abilities
|
|
127
|
+
</div>
|
|
91
128
|
</div>
|
|
92
129
|
</div>
|
|
93
130
|
{hasAbilities && (
|
|
94
|
-
<div className="
|
|
131
|
+
<div className="mt-4 flex flex-wrap gap-2">
|
|
95
132
|
{abilities.map((a) => (
|
|
96
|
-
<span
|
|
97
|
-
|
|
133
|
+
<span
|
|
134
|
+
key={a.displayName}
|
|
135
|
+
className="inline-flex items-center gap-2 rounded-full border border-slate-200 bg-slate-50 py-1 pl-1 pr-3 text-sm font-medium text-slate-700 dark:border-slate-700 dark:bg-slate-800/60 dark:text-slate-200"
|
|
136
|
+
>
|
|
137
|
+
<span
|
|
138
|
+
style={{ background: a.iconColor }}
|
|
139
|
+
className="flex h-6 w-6 items-center justify-center rounded-full text-xs font-bold text-white"
|
|
140
|
+
>
|
|
98
141
|
{a.initial}
|
|
99
142
|
</span>
|
|
100
143
|
{a.displayName}
|
|
@@ -104,18 +147,33 @@ export default function DetailPage({
|
|
|
104
147
|
)}
|
|
105
148
|
</div>
|
|
106
149
|
|
|
107
|
-
<div className="
|
|
108
|
-
<div className="
|
|
109
|
-
<h2 className="
|
|
110
|
-
|
|
150
|
+
<div className="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
|
151
|
+
<div className="mb-4 flex items-center justify-between">
|
|
152
|
+
<h2 className="text-lg font-extrabold tracking-tight text-slate-900 dark:text-white">
|
|
153
|
+
Base stats
|
|
154
|
+
</h2>
|
|
155
|
+
<span className="inline-flex items-center gap-1 rounded-full bg-slate-100 px-3 py-1 text-xs font-bold tabular-nums text-slate-500 dark:bg-slate-800 dark:text-slate-300">
|
|
156
|
+
<Sigma size={12} isr={{ key: 'LcIconSigma12' }} />
|
|
157
|
+
{statTotal}
|
|
158
|
+
</span>
|
|
111
159
|
</div>
|
|
112
|
-
<div className="
|
|
160
|
+
<div className="space-y-3">
|
|
113
161
|
{stats.map((st) => (
|
|
114
|
-
<div
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
162
|
+
<div
|
|
163
|
+
key={st.label}
|
|
164
|
+
className="grid grid-cols-[3rem_2.5rem_1fr] items-center gap-3"
|
|
165
|
+
>
|
|
166
|
+
<span className="text-xs font-semibold uppercase tracking-wide text-slate-400">
|
|
167
|
+
{st.label}
|
|
168
|
+
</span>
|
|
169
|
+
<span className="text-sm font-bold tabular-nums text-slate-700 dark:text-slate-200">
|
|
170
|
+
{st.base}
|
|
171
|
+
</span>
|
|
172
|
+
<div className="h-2.5 overflow-hidden rounded-full bg-slate-100 dark:bg-slate-800">
|
|
173
|
+
<div
|
|
174
|
+
className="h-full rounded-full"
|
|
175
|
+
style={{ width: st.barWidth, background: st.barColor }}
|
|
176
|
+
/>
|
|
119
177
|
</div>
|
|
120
178
|
</div>
|
|
121
179
|
))}
|
|
@@ -125,42 +183,69 @@ export default function DetailPage({
|
|
|
125
183
|
</div>
|
|
126
184
|
|
|
127
185
|
{hasEvolution && (
|
|
128
|
-
<div className="
|
|
129
|
-
<
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
โหลดใน loader (native route stream{' '}
|
|
134
|
-
<code className="dex-code"><Suspense></code> ไม่ได้ — ดู GAPS S3)
|
|
135
|
-
</div>
|
|
136
|
-
</div>
|
|
137
|
-
</div>
|
|
138
|
-
<div className="aa-section__body dex-evo__body">
|
|
186
|
+
<div className="rounded-3xl border border-slate-200 bg-white p-6 shadow-sm dark:border-slate-800 dark:bg-slate-900">
|
|
187
|
+
<h2 className="mb-5 text-lg font-extrabold tracking-tight text-slate-900 dark:text-white">
|
|
188
|
+
Evolution chain
|
|
189
|
+
</h2>
|
|
190
|
+
<div className="flex flex-wrap items-center justify-center gap-2">
|
|
139
191
|
{evolution.map((s) => (
|
|
140
|
-
<div key={s.id} className="
|
|
141
|
-
{
|
|
142
|
-
<
|
|
143
|
-
|
|
144
|
-
|
|
192
|
+
<div key={s.id} className="flex items-center gap-2">
|
|
193
|
+
{s.isFirst ? (
|
|
194
|
+
<span />
|
|
195
|
+
) : (
|
|
196
|
+
<div className="flex flex-col items-center px-1 text-slate-400">
|
|
197
|
+
<span className="text-xl leading-none">›</span>
|
|
198
|
+
{s.showLevel && (
|
|
199
|
+
<span className="text-[10px] font-semibold uppercase tracking-wide">
|
|
200
|
+
{s.levelLabel}
|
|
201
|
+
</span>
|
|
202
|
+
)}
|
|
145
203
|
</div>
|
|
146
204
|
)}
|
|
147
|
-
|
|
148
|
-
<
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
205
|
+
{s.isCurrent ? (
|
|
206
|
+
<a
|
|
207
|
+
href={s.detailHref}
|
|
208
|
+
className="flex w-28 flex-col items-center rounded-2xl border-2 border-brand-500 bg-brand-50/60 p-3 no-underline shadow-sm dark:border-brand-500 dark:bg-brand-500/10"
|
|
209
|
+
>
|
|
210
|
+
<img
|
|
211
|
+
src={s.artwork}
|
|
212
|
+
alt={s.displayName}
|
|
213
|
+
loading="lazy"
|
|
214
|
+
className="h-20 w-20 object-contain"
|
|
215
|
+
/>
|
|
216
|
+
<div className="text-[11px] font-semibold tabular-nums text-slate-400">
|
|
217
|
+
{s.num}
|
|
218
|
+
</div>
|
|
219
|
+
<div className="text-sm font-bold text-slate-900 dark:text-white">
|
|
220
|
+
{s.displayName}
|
|
221
|
+
</div>
|
|
222
|
+
</a>
|
|
223
|
+
) : (
|
|
224
|
+
<a
|
|
225
|
+
href={s.detailHref}
|
|
226
|
+
className="flex w-28 flex-col items-center rounded-2xl border border-slate-200 bg-white p-3 no-underline shadow-sm transition-all hover:-translate-y-0.5 hover:border-brand-500/50 dark:border-slate-800 dark:bg-slate-900"
|
|
227
|
+
>
|
|
228
|
+
<img
|
|
229
|
+
src={s.artwork}
|
|
230
|
+
alt={s.displayName}
|
|
231
|
+
loading="lazy"
|
|
232
|
+
className="h-20 w-20 object-contain"
|
|
233
|
+
/>
|
|
234
|
+
<div className="text-[11px] font-semibold tabular-nums text-slate-400">
|
|
235
|
+
{s.num}
|
|
236
|
+
</div>
|
|
237
|
+
<div className="text-sm font-semibold text-slate-800 dark:text-slate-100">
|
|
238
|
+
{s.displayName}
|
|
239
|
+
</div>
|
|
240
|
+
</a>
|
|
241
|
+
)}
|
|
157
242
|
</div>
|
|
158
243
|
))}
|
|
159
244
|
</div>
|
|
160
245
|
</div>
|
|
161
246
|
)}
|
|
162
|
-
|
|
247
|
+
</div>
|
|
163
248
|
)}
|
|
164
|
-
|
|
249
|
+
</section>
|
|
165
250
|
)
|
|
166
251
|
}
|