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.
@@ -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. They now support conditionals (S11), `style={{…}}` object attrs
5
- // (S1), and dynamic `<BrustPage>` head props (S8) all FIXED. Still
6
- // precomputed in the loader: formatted strings, helper-derived values, and
7
- // multi-property style strings (templates have no template-literals / arithmetic
8
- // / helper calls). See ../FRAMEWORK-GAPS.md.
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 (Approach a native <Outlet/> nesting). AppLayout takes NO props at
12
- * its call site (`<AppLayout native>`), so each leaf loader returns these
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 list cell — derived from the list endpoint alone (no detail fetch,
25
- * see FRAMEWORK-GAPS.md S2 / N+1 avoidance). */
26
- export interface CardVM {
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
- className: string // "dex-type dex-type--grass"
42
+ color: string // hex tint — fed into an inline style value
54
43
  }
55
44
 
56
- export interface StatVM {
57
- label: string // "HP" / "Atk" / …
58
- base: number
59
- barWidth: string // "62%" fed into `style={{ width: barWidth }}` (S1)
60
- barClassName: string // "dex-statbar__fill dex-statbar__fill--mid"
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 // type tint — fed into `style={{ background: iconColor }}` (S1)
57
+ iconColor: string // hex tint — style={{ background }}
67
58
  }
68
59
 
69
- export interface EvolutionStageVM {
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
- // Native routes now support per-item conditionals (GAPS S11 closed): the
78
- // template tests these booleans (`{!s.isFirst && <Arrow/>}`) instead of
79
- // toggling a precomputed `dex-hide` class.
80
- isFirst: boolean // true on the first stage (no leading arrow)
81
- showLevel: boolean // true when this stage has a min level to show
82
- cardClassName: string // adds the "current" highlight when this is the open Pokémon
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
- /** The full view-model handed to DetailPage. Every field is render-ready. */
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 value for `style={{ background: heroBg }}` type-tinted
107
+ heroBg: string // CSS gradient string built in the loader from the type tint
105
108
  types: TypeBadgeVM[]
106
- stats: StatVM[]
109
+ stats: StatBarVM[]
107
110
  statTotal: number
108
111
  abilities: AbilityVM[]
109
112
  hasAbilities: boolean
110
- evolution: EvolutionStageVM[]
113
+ evolution: EvoStageVM[]
111
114
  hasEvolution: boolean
112
- // native interactive props: a single string path each (native props can't be
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 // "dex-tc__cell dex-tc__cell--super"
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 (header row + one row per attacking type). The
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 // row index as string
142
- cells: TypeChartCellVM[] // 19 cells (1 head + 18)
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[] // 19 rows (1 header + 18), each 19 cells
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
- // Route "/pokemon/{name}" — NATIVE detail leaf route, rendered into AppLayout's
2
- // <Outlet/> slot (chrome lives in AppLayout). Returns JUST its inner aa-content
3
- // fragmentno <BrustPage>, no <main>. Per the decision to push native as far
4
- // as possible, the evolution chain is loaded BLOCKING in the loader (a
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 routes now support conditionals (S11): the page uses a real
9
- // `{notFound ? <NotFound/> : <Content/>}` branch, `{hasAbilities && …}` /
10
- // `{hasEvolution && …}` sections, and per-item `{!s.isFirst && <Arrow/>}` /
11
- // `{s.showLevel && <Level/>}` separators no more loader-computed hide-classes.
12
- // The <title> is dynamic via AppLayout's `<BrustPage title={title}>` (S8, the
13
- // loader merges `title` into the shared context) and inline styles use
14
- // `style={{…}}` objects (S1).
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
- <a className="aa-btn aa-btn--ghost aa-btn--sm dex-back" href="/">
41
- Pokédex
42
- </a>
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="dex-notfound">
46
- <div className="dex-notfound__code">404</div>
47
- <h2 className="aa-h3">No Pokémon named “{displayName}”</h2>
48
- <p className="dex-notfound__desc">
49
- loader ได้ 404 จาก PokeAPI. brust ยังไม่มี notFound() sentinel — หน้านี้จึงตอบ HTTP 200 พร้อม
50
- body นี้ (ดู FRAMEWORK-GAPS.md S9).
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 className="aa-btn" href="/">
53
- ‹ Back to Pokédex
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="dex-detail-grid">
59
- <div className="aa-card dex-hero" style={{ background: heroBg }}>
60
- <div className="dex-hero__num">{num}</div>
61
- <h1 className="dex-hero__name">{displayName}</h1>
62
- <div className="dex-hero__genus">{genus}</div>
63
- <div className="dex-hero__art">
64
- <img src={artwork} alt={displayName} className="dex-hero__img" />
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="dex-hero__types">
83
+ <div className="mt-4 flex flex-wrap items-center justify-center gap-2">
67
84
  {types.map((t) => (
68
- <span key={t.label} className={t.className}>
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
- <AddToTeamButton native data={addProps} />
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="dex-detail-right">
77
- <div className="aa-card aa-card--padded">
78
- <p className="dex-flavor">{flavorText}</p>
79
- <div className="dex-measures">
80
- <div className="dex-measure">
81
- <div className="dex-measure__v">{heightLabel}</div>
82
- <div className="dex-measure__k">Height</div>
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="dex-measure">
85
- <div className="dex-measure__v">{weightLabel}</div>
86
- <div className="dex-measure__k">Weight</div>
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="dex-measure">
89
- <div className="dex-measure__v">{abilityCount}</div>
90
- <div className="dex-measure__k">Abilities</div>
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="dex-abilities">
131
+ <div className="mt-4 flex flex-wrap gap-2">
95
132
  {abilities.map((a) => (
96
- <span key={a.displayName} className="aa-chip">
97
- <span className="aa-chip__icon" style={{ background: a.iconColor }}>
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="aa-section dex-stats">
108
- <div className="aa-section__head">
109
- <h2 className="aa-section__title">Base stats</h2>
110
- <span className="dex-stats__total">Σ {statTotal}</span>
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="aa-section__body dex-stats__body">
160
+ <div className="space-y-3">
113
161
  {stats.map((st) => (
114
- <div key={st.label} className="dex-statbar">
115
- <span className="dex-statbar__label">{st.label}</span>
116
- <span className="dex-statbar__val">{st.base}</span>
117
- <div className="dex-statbar__track">
118
- <div className={st.barClassName} style={{ width: st.barWidth }} />
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="aa-section dex-evo">
129
- <div className="aa-section__head">
130
- <div>
131
- <h2 className="aa-section__title">Evolution chain</h2>
132
- <div className="aa-section__desc">
133
- โหลดใน loader (native route stream{' '}
134
- <code className="dex-code">&lt;Suspense&gt;</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="dex-evo__stage">
141
- {!s.isFirst && (
142
- <div className="dex-evo__sep">
143
- <span className="dex-evo__arrow">›</span>
144
- {s.showLevel && <span className="dex-evo__lv">{s.levelLabel}</span>}
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
- <a className={s.cardClassName} href={s.detailHref}>
148
- <img
149
- className="dex-evo__img"
150
- src={s.artwork}
151
- alt={s.displayName}
152
- loading="lazy"
153
- />
154
- <div className="dex-evo__num">{s.num}</div>
155
- <div className="dex-evo__name">{s.displayName}</div>
156
- </a>
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
  }