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
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
// Rendered with `ssr` so the initial team (from the loader) ships in the HTML
|
|
4
4
|
// and the dock count is correct on first paint, then hydrates. Stays in sync
|
|
5
5
|
// with AddToTeamButton through the shared `teamStore` (one window singleton — see
|
|
6
|
-
// ../stores/team.ts
|
|
6
|
+
// ../stores/team.ts).
|
|
7
|
+
import { Users, X } from 'lucide-react'
|
|
7
8
|
import { useEffect, useState } from 'react'
|
|
8
9
|
import { client, useStore } from 'brustjs/client'
|
|
9
10
|
import type { Actions } from '../actions'
|
|
@@ -19,24 +20,24 @@ export default function TeamBuilder({ teamInitial }: { teamInitial: TeamMember[]
|
|
|
19
20
|
const [mounted, setMounted] = useState(false)
|
|
20
21
|
const [open, setOpen] = useState(false)
|
|
21
22
|
|
|
22
|
-
// This island is `ssr`, but
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
// server-seeded native snapshot is Spec B.)
|
|
23
|
+
// This island is `ssr`, but the store is empty during SSR. Drive the first
|
|
24
|
+
// render (server + client) from the `teamInitial` prop so the dock count is
|
|
25
|
+
// correct on first paint AND the hydration markup matches; switch to the live
|
|
26
|
+
// store once mounted.
|
|
27
27
|
const team = mounted ? (members ?? []) : (teamInitial ?? [])
|
|
28
28
|
|
|
29
29
|
useEffect(() => {
|
|
30
30
|
if (teamInitial?.length) teamStore.members.set(teamInitial)
|
|
31
31
|
setMounted(true)
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
32
|
+
api.team
|
|
33
|
+
.get()
|
|
34
|
+
.then((r) => {
|
|
35
|
+
if (r.data) teamStore.members.set(r.data.team)
|
|
36
|
+
})
|
|
37
|
+
.catch(console.error)
|
|
36
38
|
}, [teamInitial])
|
|
37
39
|
|
|
38
40
|
async function remove(id: number) {
|
|
39
|
-
// `.delete({})` — empty body is required (bodyless DELETE → 411). See GAPS S12.
|
|
40
41
|
const { data } = await api.team({ id }).delete({})
|
|
41
42
|
if (data) teamStore.members.set(data.team)
|
|
42
43
|
}
|
|
@@ -44,150 +45,75 @@ export default function TeamBuilder({ teamInitial }: { teamInitial: TeamMember[]
|
|
|
44
45
|
const coverage = [...new Set(team.flatMap((m) => m.types))]
|
|
45
46
|
|
|
46
47
|
return (
|
|
47
|
-
<div
|
|
48
|
+
<div className="fixed bottom-5 right-5 z-[200]">
|
|
48
49
|
{open && (
|
|
49
|
-
<div
|
|
50
|
-
className="
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
border: '1px solid var(--border-default)',
|
|
56
|
-
}}
|
|
57
|
-
>
|
|
58
|
-
<div style={{ height: 3, background: 'var(--aa-gradient)' }} />
|
|
59
|
-
<div
|
|
60
|
-
style={{
|
|
61
|
-
padding: '14px 16px',
|
|
62
|
-
borderBottom: '1px solid var(--border-subtle)',
|
|
63
|
-
display: 'flex',
|
|
64
|
-
alignItems: 'center',
|
|
65
|
-
gap: 8,
|
|
66
|
-
}}
|
|
67
|
-
>
|
|
68
|
-
<span
|
|
69
|
-
style={{
|
|
70
|
-
fontFamily: 'var(--font-display)',
|
|
71
|
-
fontWeight: 800,
|
|
72
|
-
fontSize: 'var(--text-sm)',
|
|
73
|
-
}}
|
|
74
|
-
>
|
|
75
|
-
My team
|
|
76
|
-
</span>
|
|
77
|
-
<span className="aa-pill aa-pill--muted aa-pill--no-dot" style={{ marginLeft: 'auto' }}>
|
|
50
|
+
<div className="mb-3 w-80 overflow-hidden rounded-xl border border-slate-200 bg-white shadow-2xl dark:border-slate-700 dark:bg-slate-900">
|
|
51
|
+
<div className="h-1 bg-brand-500" />
|
|
52
|
+
<div className="flex items-center gap-2 border-b border-slate-100 px-4 py-3 dark:border-slate-800">
|
|
53
|
+
<Users size={16} className="text-brand-500" />
|
|
54
|
+
<span className="text-sm font-extrabold text-slate-900 dark:text-white">My team</span>
|
|
55
|
+
<span className="ml-auto rounded-full bg-slate-100 px-2 py-0.5 text-xs font-semibold text-slate-600 dark:bg-slate-800 dark:text-slate-300">
|
|
78
56
|
{team.length} / {MAX}
|
|
79
57
|
</span>
|
|
80
58
|
</div>
|
|
81
59
|
|
|
82
60
|
{team.length === 0 ? (
|
|
83
|
-
<div
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
textAlign: 'center',
|
|
87
|
-
color: 'var(--text-tertiary)',
|
|
88
|
-
fontSize: 'var(--text-xs)',
|
|
89
|
-
lineHeight: 1.6,
|
|
90
|
-
}}
|
|
91
|
-
>
|
|
92
|
-
ยังไม่มี Pokémon ในทีม
|
|
61
|
+
<div className="px-5 py-7 text-center text-xs leading-relaxed text-slate-400">
|
|
62
|
+
<Users size={28} className="mx-auto mb-2 text-slate-300 dark:text-slate-600" />
|
|
63
|
+
No Pokémon on your team yet.
|
|
93
64
|
<br />
|
|
94
|
-
|
|
65
|
+
Open a detail page and tap{' '}
|
|
66
|
+
<b className="text-slate-600 dark:text-slate-200">Add to team</b>.
|
|
95
67
|
</div>
|
|
96
68
|
) : (
|
|
97
69
|
<>
|
|
98
|
-
<div
|
|
70
|
+
<div className="max-h-72 overflow-y-auto">
|
|
99
71
|
{team.map((m) => (
|
|
100
72
|
<div
|
|
101
73
|
key={m.id}
|
|
102
|
-
|
|
103
|
-
display: 'flex',
|
|
104
|
-
alignItems: 'center',
|
|
105
|
-
gap: 10,
|
|
106
|
-
padding: '9px 14px',
|
|
107
|
-
borderBottom: '1px solid var(--border-subtle)',
|
|
108
|
-
}}
|
|
74
|
+
className="flex items-center gap-2.5 border-b border-slate-100 px-3.5 py-2.5 dark:border-slate-800"
|
|
109
75
|
>
|
|
110
|
-
<div
|
|
111
|
-
|
|
112
|
-
width: 36,
|
|
113
|
-
height: 36,
|
|
114
|
-
borderRadius: 'var(--radius-md)',
|
|
115
|
-
background: 'var(--surface-sunken)',
|
|
116
|
-
display: 'grid',
|
|
117
|
-
placeItems: 'center',
|
|
118
|
-
flex: 'none',
|
|
119
|
-
}}
|
|
120
|
-
>
|
|
121
|
-
<img
|
|
122
|
-
src={m.artwork}
|
|
123
|
-
alt={m.displayName}
|
|
124
|
-
style={{ width: 30, height: 30, objectFit: 'contain' }}
|
|
125
|
-
/>
|
|
76
|
+
<div className="grid h-9 w-9 flex-none place-items-center rounded-md bg-slate-100 dark:bg-slate-800">
|
|
77
|
+
<img src={m.artwork} alt={m.displayName} className="h-7 w-7 object-contain" />
|
|
126
78
|
</div>
|
|
127
|
-
<a
|
|
128
|
-
|
|
129
|
-
style={{ flex: 1, minWidth: 0, textDecoration: 'none' }}
|
|
130
|
-
>
|
|
131
|
-
<div
|
|
132
|
-
style={{
|
|
133
|
-
fontWeight: 600,
|
|
134
|
-
fontSize: 'var(--text-xs)',
|
|
135
|
-
color: 'var(--text-primary)',
|
|
136
|
-
}}
|
|
137
|
-
>
|
|
79
|
+
<a href={`/pokemon/${m.name}`} className="min-w-0 flex-1 no-underline">
|
|
80
|
+
<div className="text-xs font-semibold text-slate-900 dark:text-white">
|
|
138
81
|
{m.displayName}
|
|
139
82
|
</div>
|
|
140
|
-
<div
|
|
141
|
-
style={{
|
|
142
|
-
fontFamily: 'var(--font-mono)',
|
|
143
|
-
fontSize: 'var(--text-3xs)',
|
|
144
|
-
color: 'var(--text-tertiary)',
|
|
145
|
-
}}
|
|
146
|
-
>
|
|
147
|
-
{m.num}
|
|
148
|
-
</div>
|
|
83
|
+
<div className="font-mono text-[0.625rem] text-slate-400">{m.num}</div>
|
|
149
84
|
</a>
|
|
150
|
-
<div
|
|
85
|
+
<div className="flex gap-1">
|
|
151
86
|
{m.types.map((t) => (
|
|
152
|
-
<span
|
|
87
|
+
<span
|
|
88
|
+
key={t}
|
|
89
|
+
className="rounded bg-slate-100 px-1.5 py-0.5 text-[0.625rem] font-medium text-slate-600 dark:bg-slate-800 dark:text-slate-300"
|
|
90
|
+
>
|
|
153
91
|
{t}
|
|
154
92
|
</span>
|
|
155
93
|
))}
|
|
156
94
|
</div>
|
|
157
95
|
<button
|
|
158
96
|
type="button"
|
|
159
|
-
className="
|
|
97
|
+
className="rounded p-1 text-slate-400 hover:bg-slate-100 hover:text-slate-700 dark:hover:bg-slate-800"
|
|
160
98
|
onClick={() => remove(m.id)}
|
|
161
99
|
aria-label="Remove"
|
|
162
100
|
>
|
|
163
|
-
|
|
101
|
+
<X size={14} />
|
|
164
102
|
</button>
|
|
165
103
|
</div>
|
|
166
104
|
))}
|
|
167
105
|
</div>
|
|
168
106
|
{coverage.length > 0 && (
|
|
169
|
-
<div
|
|
170
|
-
|
|
171
|
-
padding: '11px 14px',
|
|
172
|
-
background: 'var(--ink-25)',
|
|
173
|
-
borderTop: '1px solid var(--border-subtle)',
|
|
174
|
-
}}
|
|
175
|
-
>
|
|
176
|
-
<div
|
|
177
|
-
style={{
|
|
178
|
-
fontSize: 'var(--text-3xs)',
|
|
179
|
-
fontWeight: 700,
|
|
180
|
-
color: 'var(--text-tertiary)',
|
|
181
|
-
textTransform: 'uppercase',
|
|
182
|
-
letterSpacing: '0.08em',
|
|
183
|
-
marginBottom: 7,
|
|
184
|
-
}}
|
|
185
|
-
>
|
|
107
|
+
<div className="border-t border-slate-100 bg-slate-50 px-3.5 py-3 dark:border-slate-800 dark:bg-slate-800/40">
|
|
108
|
+
<div className="mb-2 text-[0.625rem] font-bold uppercase tracking-wider text-slate-400">
|
|
186
109
|
Type coverage
|
|
187
110
|
</div>
|
|
188
|
-
<div
|
|
111
|
+
<div className="flex flex-wrap gap-1.5">
|
|
189
112
|
{coverage.map((t) => (
|
|
190
|
-
<span
|
|
113
|
+
<span
|
|
114
|
+
key={t}
|
|
115
|
+
className="rounded bg-slate-100 px-1.5 py-0.5 text-[0.625rem] font-medium text-slate-600 dark:bg-slate-800 dark:text-slate-300"
|
|
116
|
+
>
|
|
191
117
|
{t}
|
|
192
118
|
</span>
|
|
193
119
|
))}
|
|
@@ -201,21 +127,12 @@ export default function TeamBuilder({ teamInitial }: { teamInitial: TeamMember[]
|
|
|
201
127
|
|
|
202
128
|
<button
|
|
203
129
|
type="button"
|
|
204
|
-
className="
|
|
130
|
+
className="inline-flex items-center gap-1.5 rounded-full bg-brand-500 px-5 py-2.5 text-sm font-semibold text-white shadow-lg shadow-brand-500/30 transition-colors hover:bg-brand-600"
|
|
205
131
|
onClick={() => setOpen((o) => !o)}
|
|
206
|
-
style={{ boxShadow: 'var(--shadow-brand)', paddingInline: 18 }}
|
|
207
132
|
>
|
|
133
|
+
<Users size={16} />
|
|
208
134
|
My team
|
|
209
|
-
<span
|
|
210
|
-
style={{
|
|
211
|
-
marginLeft: 6,
|
|
212
|
-
padding: '1px 8px',
|
|
213
|
-
borderRadius: 'var(--radius-pill)',
|
|
214
|
-
background: 'rgba(255,255,255,0.22)',
|
|
215
|
-
fontWeight: 800,
|
|
216
|
-
fontSize: 'var(--text-xs)',
|
|
217
|
-
}}
|
|
218
|
-
>
|
|
135
|
+
<span className="rounded-full bg-white/25 px-2 py-0.5 text-xs font-extrabold">
|
|
219
136
|
{team.length}
|
|
220
137
|
</span>
|
|
221
138
|
</button>
|
|
@@ -1,14 +1,15 @@
|
|
|
1
|
-
// NATIVE INTERACTIVE COMPONENT
|
|
2
|
-
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
1
|
+
// NATIVE INTERACTIVE COMPONENT — the dark/light theme toggle in the navbar.
|
|
2
|
+
// Single-file native directive component: a co-located `export const behavior`
|
|
3
|
+
// (client logic, react-free) + a JSX `default` export (the native template the
|
|
4
|
+
// compiler lowers to minijinja). The build bundles ONLY `behavior` into
|
|
5
|
+
// _directives.js, registered as "themeToggle" (camelCase filename); the JSX
|
|
6
|
+
// default is tree-shaken out so react never leaks client-side.
|
|
7
7
|
//
|
|
8
8
|
// react-free: `signal`/`computed` from brustjs/store (the window singleton on
|
|
9
9
|
// the client), `client` from brustjs/client (the treaty action client). The
|
|
10
10
|
// toggle flips <html data-mode> immediately (no reload) AND persists via the
|
|
11
11
|
// /theme action which sets the `mode` cookie — so SSR matches on the next load.
|
|
12
|
+
import { Moon, Sun } from 'lucide-react'
|
|
12
13
|
import { client } from 'brustjs/client'
|
|
13
14
|
import { computed, signal } from 'brustjs/store'
|
|
14
15
|
import type { Actions } from '../actions'
|
|
@@ -21,7 +22,11 @@ export const behavior = () => {
|
|
|
21
22
|
const mode = signal(
|
|
22
23
|
typeof document !== 'undefined' ? (document.documentElement.dataset.mode ?? 'dark') : 'dark',
|
|
23
24
|
)
|
|
24
|
-
const label = computed(() => (mode() === 'dark' ? '
|
|
25
|
+
const label = computed(() => (mode() === 'dark' ? 'Light' : 'Dark'))
|
|
26
|
+
// x-text can't host an SSR icon (it replaces textContent), so the sun/moon
|
|
27
|
+
// icons live in sibling spans toggled by x-show on these mode computeds.
|
|
28
|
+
const isDark = computed(() => mode() === 'dark')
|
|
29
|
+
const isLight = computed(() => mode() === 'light')
|
|
25
30
|
|
|
26
31
|
async function toggle() {
|
|
27
32
|
const next = mode() === 'dark' ? 'light' : 'dark'
|
|
@@ -30,7 +35,7 @@ export const behavior = () => {
|
|
|
30
35
|
await api.theme.post({ mode: next }) // persist via cookie for the next SSR
|
|
31
36
|
}
|
|
32
37
|
|
|
33
|
-
return { toggle, label }
|
|
38
|
+
return { toggle, label, isDark, isLight }
|
|
34
39
|
}
|
|
35
40
|
|
|
36
41
|
// default → jinja (server). The x-* directives are static string attributes the
|
|
@@ -41,11 +46,17 @@ export default function ThemeToggle() {
|
|
|
41
46
|
<button
|
|
42
47
|
type="button"
|
|
43
48
|
x-data="themeToggle"
|
|
44
|
-
x-text="label"
|
|
45
49
|
x-on-click="toggle"
|
|
46
|
-
|
|
50
|
+
aria-label="Toggle theme"
|
|
51
|
+
className="inline-flex items-center gap-1.5 rounded-lg border border-slate-200 px-3 py-1.5 text-sm font-medium text-slate-700 transition-colors hover:bg-slate-100 dark:border-slate-700 dark:text-slate-200 dark:hover:bg-slate-800"
|
|
47
52
|
>
|
|
48
|
-
|
|
53
|
+
<span x-show="isDark" className="inline-flex">
|
|
54
|
+
<Sun size={16} isr={{ key: 'LcIconSun' }} />
|
|
55
|
+
</span>
|
|
56
|
+
<span x-show="isLight" className="inline-flex">
|
|
57
|
+
<Moon size={16} isr={{ key: 'LcIconMoon' }} />
|
|
58
|
+
</span>
|
|
59
|
+
<span x-text="label">Dark</span>
|
|
49
60
|
</button>
|
|
50
61
|
)
|
|
51
62
|
}
|