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.
@@ -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 / GAPS S4; replaced the old window-event bus).
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 Spec A does not server-seed native client state, so
23
- // the store is empty during SSR. Drive the first render (server + client) from
24
- // the `teamInitial` prop so the dock count is correct on first paint AND the
25
- // hydration markup matches; switch to the live store once mounted. (Full
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
- // Re-sync from the server on mount (the SSR snapshot may be stale by now).
33
- api.team.get().then((r) => {
34
- if (r.data) teamStore.members.set(r.data.team)
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 style={{ position: 'fixed', right: 20, bottom: 20, zIndex: 200 }}>
48
+ <div className="fixed bottom-5 right-5 z-[200]">
48
49
  {open && (
49
- <div
50
- className="aa-card"
51
- style={{
52
- width: 320,
53
- marginBottom: 12,
54
- boxShadow: 'var(--shadow-xl)',
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
- style={{
85
- padding: '28px 20px',
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
- เปิดหน้า detail แล้วกด <b style={{ color: 'var(--text-secondary)' }}>Add to team</b>
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 style={{ maxHeight: 300, overflowY: 'auto' }}>
70
+ <div className="max-h-72 overflow-y-auto">
99
71
  {team.map((m) => (
100
72
  <div
101
73
  key={m.id}
102
- style={{
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
- style={{
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
- href={`/pokemon/${m.name}`}
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 style={{ display: 'flex', gap: 4 }}>
85
+ <div className="flex gap-1">
151
86
  {m.types.map((t) => (
152
- <span key={t} className={`dex-type dex-type--${t} dex-type--sm`}>
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="aa-btn aa-btn--ghost aa-btn--icon aa-btn--xs"
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
- style={{
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 style={{ display: 'flex', flexWrap: 'wrap', gap: 5 }}>
111
+ <div className="flex flex-wrap gap-1.5">
189
112
  {coverage.map((t) => (
190
- <span key={t} className={`dex-type dex-type--${t} dex-type--sm`}>
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="aa-btn aa-btn--lg"
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 (B4 dogfood) — the dark/light theme toggle in
2
- // the topbar. Single-file native directive component: a co-located
3
- // `export const behavior` (client logic, react-free) + a JSX `default` export
4
- // (the native template the compiler lowers to minijinja). The build bundles
5
- // ONLY `behavior` into _directives.js, registered as "themeToggle" (camelCase
6
- // filename); the JSX default is tree-shaken out so react never leaks client-side.
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' ? 'Light' : '🌙 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
- className="aa-btn aa-btn--outline aa-btn--sm"
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
- 🌙 Dark
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
  }