codeblog-app 2.5.0 → 2.6.0

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,200 +1,223 @@
1
- import { createSignal } from "solid-js"
2
- import { useKeyboard } from "@opentui/solid"
3
- import { useTheme, THEME_NAMES, THEMES } from "../context/theme"
1
+ import { createSignal, createMemo } from "solid-js"
2
+ import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
3
+ import { useTheme, THEME_NAMES, THEMES, type ThemeColors } from "../context/theme"
4
4
 
5
- // High-contrast colors that are visible on ANY terminal background
6
5
  const HC = {
7
6
  title: "#ff6600",
8
- text: "#888888",
9
- selected: "#00cc00",
10
- dim: "#999999",
7
+ text: "#aaaaaa",
8
+ dim: "#aaaaaa",
11
9
  }
12
10
 
11
+ const LOGO_ORANGE = "#f48225"
12
+ const LOGO_CYAN = "#00c8ff"
13
+
14
+ const LOGO = [
15
+ " ██████╗ ██████╗ ██████╗ ███████╗██████╗ ██╗ ██████╗ ██████╗ ",
16
+ "██╔════╝██╔═══██╗██╔══██╗██╔════╝██╔══██╗██║ ██╔═══██╗██╔════╝ ",
17
+ "██║ ██║ ██║██║ ██║█████╗ ██████╔╝██║ ██║ ██║██║ ███╗",
18
+ "██║ ██║ ██║██║ ██║██╔══╝ ██╔══██╗██║ ██║ ██║██║ ██║",
19
+ "╚██████╗╚██████╔╝██████╔╝███████╗██████╔╝███████╗╚██████╔╝╚██████╔╝",
20
+ " ╚═════╝ ╚═════╝ ╚═════╝ ╚══════╝╚═════╝ ╚══════╝ ╚═════╝ ╚═════╝ ",
21
+ ]
22
+
13
23
  function resolveThemeDef(name: string) {
14
24
  const fallback = THEMES.codeblog ?? Object.values(THEMES).find(Boolean)
15
- if (!fallback) {
16
- throw new Error("No themes available")
17
- }
25
+ if (!fallback) throw new Error("No themes available")
18
26
  return THEMES[name] ?? fallback
19
27
  }
20
28
 
21
- export function ThemeSetup() {
29
+ type ThemeOption = { name: string; mode: "dark" | "light"; label: string; colors: ThemeColors }
30
+
31
+ const SETUP_OPTIONS: ThemeOption[] = [
32
+ { name: "codeblog", mode: "dark", label: "Dark mode", colors: resolveThemeDef("codeblog").dark },
33
+ { name: "codeblog", mode: "light", label: "Light mode", colors: resolveThemeDef("codeblog").light },
34
+ { name: "dracula", mode: "dark", label: "Dark — Dracula", colors: resolveThemeDef("dracula").dark },
35
+ { name: "tokyonight", mode: "dark", label: "Dark — Tokyo Night", colors: resolveThemeDef("tokyonight").dark },
36
+ { name: "catppuccin", mode: "dark", label: "Dark — Catppuccin", colors: resolveThemeDef("catppuccin").dark },
37
+ { name: "github", mode: "dark", label: "Dark — GitHub", colors: resolveThemeDef("github").dark },
38
+ { name: "gruvbox", mode: "dark", label: "Dark — Gruvbox", colors: resolveThemeDef("gruvbox").dark },
39
+ { name: "github", mode: "light", label: "Light — GitHub", colors: resolveThemeDef("github").light },
40
+ { name: "catppuccin", mode: "light", label: "Light — Catppuccin", colors: resolveThemeDef("catppuccin").light },
41
+ { name: "solarized", mode: "light", label: "Light — Solarized", colors: resolveThemeDef("solarized").light },
42
+ ]
43
+
44
+ export function ThemeSetup(props: { onDone?: () => void }) {
22
45
  const theme = useTheme()
23
- const modes = ["dark", "light"] as const
24
- const [step, setStep] = createSignal<"mode" | "theme">("mode")
25
- const [modeIdx, setModeIdx] = createSignal(0)
26
- const [themeIdx, setThemeIdx] = createSignal(0)
27
- const getThemeName = (index: number) => THEME_NAMES[index] ?? "codeblog"
28
- const getThemeColors = (name: string) => resolveThemeDef(name)[theme.mode]
46
+ const dimensions = useTerminalDimensions()
47
+ const [idx, setIdx] = createSignal(0)
48
+
49
+ const current = createMemo(() => SETUP_OPTIONS[idx()] ?? SETUP_OPTIONS[0]!)
50
+
51
+ function apply(i: number) {
52
+ const opt = SETUP_OPTIONS[i]
53
+ if (!opt) return
54
+ theme.set(opt.name)
55
+ theme.setMode(opt.mode)
56
+ }
57
+
58
+ apply(0)
29
59
 
30
60
  useKeyboard((evt) => {
31
- if (step() === "mode") {
32
- if (evt.name === "up" || evt.name === "k") {
33
- setModeIdx((i) => (i - 1 + modes.length) % modes.length)
34
- evt.preventDefault()
35
- return
36
- }
37
- if (evt.name === "down" || evt.name === "j") {
38
- setModeIdx((i) => (i + 1) % modes.length)
39
- evt.preventDefault()
40
- return
41
- }
42
- if (evt.name === "return") {
43
- theme.setMode(modes[modeIdx()] ?? "dark")
44
- setStep("theme")
45
- evt.preventDefault()
46
- return
47
- }
61
+ if (evt.name === "up" || evt.name === "k") {
62
+ const next = (idx() - 1 + SETUP_OPTIONS.length) % SETUP_OPTIONS.length
63
+ setIdx(next)
64
+ apply(next)
65
+ evt.preventDefault()
66
+ return
48
67
  }
49
-
50
- if (step() === "theme") {
51
- if (evt.name === "up" || evt.name === "k") {
52
- const next = (themeIdx() - 1 + THEME_NAMES.length) % THEME_NAMES.length
53
- setThemeIdx(next)
54
- theme.set(getThemeName(next))
55
- evt.preventDefault()
56
- return
57
- }
58
- if (evt.name === "down" || evt.name === "j") {
59
- const next = (themeIdx() + 1) % THEME_NAMES.length
60
- setThemeIdx(next)
61
- theme.set(getThemeName(next))
62
- evt.preventDefault()
63
- return
64
- }
65
- if (evt.name === "return") {
66
- theme.finishSetup()
67
- evt.preventDefault()
68
- return
69
- }
70
- if (evt.name === "escape") {
71
- setStep("mode")
72
- evt.preventDefault()
73
- return
74
- }
68
+ if (evt.name === "down" || evt.name === "j") {
69
+ const next = (idx() + 1) % SETUP_OPTIONS.length
70
+ setIdx(next)
71
+ apply(next)
72
+ evt.preventDefault()
73
+ return
74
+ }
75
+ if (evt.name === "return") {
76
+ theme.finishSetup()
77
+ props.onDone?.()
78
+ evt.preventDefault()
79
+ return
75
80
  }
76
81
  })
77
82
 
83
+ const wide = createMemo(() => (dimensions().width ?? 80) >= 90)
84
+ const c = createMemo(() => current().colors)
85
+
78
86
  return (
79
- <box flexDirection="column" flexGrow={1} alignItems="center" paddingLeft={2} paddingRight={2}>
87
+ <box flexDirection="column" flexGrow={1} paddingLeft={2} paddingRight={2}>
80
88
  <box flexGrow={1} minHeight={0} />
81
89
 
90
+ {/* Logo */}
91
+ <box flexShrink={0} flexDirection="column" alignItems="center">
92
+ {LOGO.map((line, i) => (
93
+ <text fg={i < 3 ? LOGO_ORANGE : LOGO_CYAN}>{line}</text>
94
+ ))}
95
+ <box height={1} />
96
+ <text fg={HC.text}>{"Agent Only Coding Society"}</text>
97
+ <box height={1} />
98
+ </box>
99
+
100
+ {/* Main content */}
82
101
  <box flexShrink={0} flexDirection="column" alignItems="center">
83
102
  <text fg={HC.title}>
84
- <span style={{ bold: true }}>{" Welcome to CodeBlog "}</span>
103
+ <span style={{ bold: true }}>{"Choose the text style that looks best with your terminal:"}</span>
85
104
  </text>
105
+ <text fg={HC.dim}>{"To change this later, run /theme"}</text>
86
106
  <box height={1} />
87
- </box>
88
107
 
89
- {step() === "mode" ? (
90
- <box flexShrink={0} flexDirection="column" width="100%" maxWidth={50}>
91
- <text fg={HC.title}>
92
- <span style={{ bold: true }}>{"What is your terminal background color?"}</span>
93
- </text>
94
- <box height={1} />
95
- {modes.map((m, i) => (
96
- <box flexDirection="row" paddingLeft={2}>
97
- <text fg={modeIdx() === i ? HC.selected : HC.dim}>
98
- {modeIdx() === i ? "❯ " : " "}
99
- </text>
100
- <text fg={modeIdx() === i ? HC.selected : HC.dim}>
101
- <span style={{ bold: modeIdx() === i }}>
102
- {m === "dark" ? "Dark background (black/dark terminal)" : "Light background (white/light terminal)"}
103
- </span>
104
- </text>
105
- </box>
106
- ))}
107
- <box height={1} />
108
- <text fg={HC.text}>{"↑↓ select · Enter confirm"}</text>
109
- </box>
110
- ) : (
111
- <box flexShrink={0} flexDirection="column" width="100%" maxWidth={60}>
112
- <text fg={theme.colors.text}>
113
- <span style={{ bold: true }}>{"Choose a color theme:"}</span>
114
- </text>
115
- <box height={1} />
116
- {THEME_NAMES.map((name, i) => {
117
- const c = getThemeColors(name)
118
- return (
119
- <box flexDirection="row" paddingLeft={2}>
120
- <text fg={themeIdx() === i ? c.primary : theme.colors.textMuted}>
121
- {themeIdx() === i ? "❯ " : " "}
108
+ <box flexDirection="row" justifyContent="center" gap={wide() ? 6 : 3}>
109
+ {/* Options list */}
110
+ <box flexDirection="column" width={wide() ? 28 : 26}>
111
+ {SETUP_OPTIONS.map((opt, i) => (
112
+ <box flexDirection="row">
113
+ <text fg={idx() === i ? opt.colors.primary : HC.dim}>
114
+ {idx() === i ? "❯ " : " "}
122
115
  </text>
123
- <text fg={themeIdx() === i ? c.text : theme.colors.textMuted}>
124
- <span style={{ bold: themeIdx() === i }}>
125
- {name.padEnd(14)}
116
+ <text fg={idx() === i ? opt.colors.text : HC.dim}>
117
+ <span style={{ bold: idx() === i }}>
118
+ {`${(i + 1).toString().padStart(2)}. ${opt.label}`}
126
119
  </span>
127
120
  </text>
128
- <text fg={c.logo1}>{""}</text>
129
- <text fg={c.logo2}>{"●"}</text>
130
- <text fg={c.primary}>{"●"}</text>
131
- <text fg={c.accent}>{"●"}</text>
132
- <text fg={c.success}>{"●"}</text>
133
- <text fg={c.error}>{"●"}</text>
121
+ {idx() === i && <text fg={opt.colors.success}>{""}</text>}
134
122
  </box>
135
- )
136
- })}
137
- <box height={1} />
138
- <text fg={theme.colors.textMuted}>{"↑↓ select · Enter confirm · Esc back"}</text>
123
+ ))}
124
+ </box>
125
+
126
+ {/* Live preview */}
127
+ <box flexDirection="column" width={wide() ? 44 : 38}>
128
+ <text fg={c().text}><span style={{ bold: true }}>{"Preview"}</span></text>
129
+ <box height={1} />
130
+ <box flexDirection="column" paddingLeft={2}>
131
+ <text fg={c().textMuted}>{"// A coding conversation"}</text>
132
+ <box height={1} />
133
+ <box flexDirection="row">
134
+ <text fg={c().primary}><span style={{ bold: true }}>{"You: "}</span></text>
135
+ <text fg={c().text}>{"Refactor the auth module"}</text>
136
+ </box>
137
+ <box flexDirection="row">
138
+ <text fg={c().accent}><span style={{ bold: true }}>{"AI: "}</span></text>
139
+ <text fg={c().text}>{"I'll update 3 files..."}</text>
140
+ </box>
141
+ <box height={1} />
142
+ <text fg={c().textMuted}>{" src/auth.ts"}</text>
143
+ <box flexDirection="row">
144
+ <text fg={c().error}>{" - "}</text>
145
+ <text fg={c().error}>{"const token = getOld()"}</text>
146
+ </box>
147
+ <box flexDirection="row">
148
+ <text fg={c().success}>{" + "}</text>
149
+ <text fg={c().success}>{"const token = getNew()"}</text>
150
+ </box>
151
+ <box height={1} />
152
+ <box flexDirection="row">
153
+ <text fg={c().success}>{"✓ "}</text>
154
+ <text fg={c().text}>{"Changes applied"}</text>
155
+ </box>
156
+ <box flexDirection="row">
157
+ <text fg={c().warning}>{"⚠ "}</text>
158
+ <text fg={c().textMuted}>{"3 tests need updating"}</text>
159
+ </box>
160
+ </box>
161
+ </box>
139
162
  </box>
140
- )}
163
+
164
+ <box height={1} />
165
+ <text fg={HC.text}>{"↑↓ select · Enter confirm"}</text>
166
+ </box>
141
167
 
142
168
  <box flexGrow={1} minHeight={0} />
143
169
  </box>
144
170
  )
145
171
  }
146
172
 
173
+ // Full theme picker (all themes × dark/light) for /theme command in main TUI
174
+ function buildAllOptions(): ThemeOption[] {
175
+ const out: ThemeOption[] = []
176
+ for (const name of THEME_NAMES) {
177
+ const def = resolveThemeDef(name)
178
+ out.push({ name, mode: "dark", label: `${name} — dark`, colors: def.dark })
179
+ out.push({ name, mode: "light", label: `${name} — light`, colors: def.light })
180
+ }
181
+ return out
182
+ }
183
+
184
+ const ALL_OPTIONS = buildAllOptions()
185
+
147
186
  export function ThemePicker(props: { onDone: () => void }) {
148
187
  const theme = useTheme()
149
- const [idx, setIdx] = createSignal(Math.max(0, THEME_NAMES.indexOf(theme.name)))
150
- const [tab, setTab] = createSignal<"theme" | "mode">("theme")
151
- const getThemeName = (index: number) => THEME_NAMES[index] ?? "codeblog"
152
- const getThemeColors = (name: string) => resolveThemeDef(name)[theme.mode]
188
+ const [idx, setIdx] = createSignal(
189
+ Math.max(0, ALL_OPTIONS.findIndex((o) => o.name === theme.name && o.mode === theme.mode))
190
+ )
191
+ const current = createMemo(() => ALL_OPTIONS[idx()] ?? ALL_OPTIONS[0]!)
192
+
193
+ function apply(i: number) {
194
+ const opt = ALL_OPTIONS[i]
195
+ if (!opt) return
196
+ theme.set(opt.name)
197
+ theme.setMode(opt.mode)
198
+ }
199
+
200
+ const c = createMemo(() => current().colors)
153
201
 
154
202
  useKeyboard((evt) => {
155
- if (tab() === "theme") {
156
- if (evt.name === "up" || evt.name === "k") {
157
- const next = (idx() - 1 + THEME_NAMES.length) % THEME_NAMES.length
158
- setIdx(next)
159
- theme.set(getThemeName(next))
160
- evt.preventDefault()
161
- return
162
- }
163
- if (evt.name === "down" || evt.name === "j") {
164
- const next = (idx() + 1) % THEME_NAMES.length
165
- setIdx(next)
166
- theme.set(getThemeName(next))
167
- evt.preventDefault()
168
- return
169
- }
170
- if (evt.name === "tab") {
171
- setTab("mode")
172
- evt.preventDefault()
173
- return
174
- }
175
- if (evt.name === "return" || evt.name === "escape") {
176
- props.onDone()
177
- evt.preventDefault()
178
- return
179
- }
203
+ if (evt.name === "up" || evt.name === "k") {
204
+ const next = (idx() - 1 + ALL_OPTIONS.length) % ALL_OPTIONS.length
205
+ setIdx(next)
206
+ apply(next)
207
+ evt.preventDefault()
208
+ return
180
209
  }
181
-
182
- if (tab() === "mode") {
183
- if (evt.name === "up" || evt.name === "down" || evt.name === "k" || evt.name === "j") {
184
- theme.setMode(theme.mode === "dark" ? "light" : "dark")
185
- evt.preventDefault()
186
- return
187
- }
188
- if (evt.name === "tab") {
189
- setTab("theme")
190
- evt.preventDefault()
191
- return
192
- }
193
- if (evt.name === "return" || evt.name === "escape") {
194
- props.onDone()
195
- evt.preventDefault()
196
- return
197
- }
210
+ if (evt.name === "down" || evt.name === "j") {
211
+ const next = (idx() + 1) % ALL_OPTIONS.length
212
+ setIdx(next)
213
+ apply(next)
214
+ evt.preventDefault()
215
+ return
216
+ }
217
+ if (evt.name === "return" || evt.name === "escape") {
218
+ props.onDone()
219
+ evt.preventDefault()
220
+ return
198
221
  }
199
222
  })
200
223
 
@@ -205,60 +228,52 @@ export function ThemePicker(props: { onDone: () => void }) {
205
228
  <span style={{ bold: true }}>Theme Settings</span>
206
229
  </text>
207
230
  <box flexGrow={1} />
208
- <text fg={theme.colors.textMuted}>{"Tab: switch section · Enter/Esc: done"}</text>
231
+ <text fg={theme.colors.textMuted}>{"Enter/Esc: done"}</text>
209
232
  </box>
210
233
 
211
234
  <box flexDirection="row" flexGrow={1} paddingTop={1} gap={4}>
212
- {/* Theme list */}
213
- <box flexDirection="column" width={40}>
214
- <text fg={tab() === "theme" ? theme.colors.text : theme.colors.textMuted}>
215
- <span style={{ bold: true }}>{"Color Theme"}</span>
216
- </text>
217
- <box height={1} />
218
- {THEME_NAMES.map((name, i) => {
219
- const c = getThemeColors(name)
220
- return (
221
- <box flexDirection="row">
222
- <text fg={idx() === i ? c.primary : theme.colors.textMuted}>
223
- {idx() === i && tab() === "theme" ? "❯ " : " "}
224
- </text>
225
- <text fg={idx() === i ? c.text : theme.colors.textMuted}>
226
- <span style={{ bold: idx() === i }}>
227
- {name.padEnd(14)}
228
- </span>
229
- </text>
230
- <text fg={c.logo1}>{" ●"}</text>
231
- <text fg={c.logo2}>{"●"}</text>
232
- <text fg={c.primary}>{"●"}</text>
233
- <text fg={c.accent}>{"●"}</text>
234
- <text fg={c.success}>{"●"}</text>
235
- <text fg={c.error}>{"●"}</text>
236
- </box>
237
- )
238
- })}
235
+ {/* Options list */}
236
+ <box flexDirection="column" width={30}>
237
+ {ALL_OPTIONS.map((opt, i) => (
238
+ <box flexDirection="row">
239
+ <text fg={idx() === i ? opt.colors.primary : theme.colors.textMuted}>
240
+ {idx() === i ? "❯ " : " "}
241
+ </text>
242
+ <text fg={idx() === i ? opt.colors.text : theme.colors.textMuted}>
243
+ <span style={{ bold: idx() === i }}>
244
+ {opt.label}
245
+ </span>
246
+ </text>
247
+ </box>
248
+ ))}
239
249
  </box>
240
250
 
241
- {/* Mode toggle */}
242
- <box flexDirection="column" width={30}>
243
- <text fg={tab() === "mode" ? theme.colors.text : theme.colors.textMuted}>
244
- <span style={{ bold: true }}>{"Background Mode"}</span>
245
- </text>
251
+ {/* Preview */}
252
+ <box flexDirection="column" width={40}>
253
+ <text fg={c().text}><span style={{ bold: true }}>{"Preview"}</span></text>
246
254
  <box height={1} />
247
- <box flexDirection="row">
248
- <text fg={theme.mode === "dark" && tab() === "mode" ? theme.colors.primary : theme.colors.textMuted}>
249
- {theme.mode === "dark" && tab() === "mode" ? "❯ " : " "}
250
- </text>
251
- <text fg={theme.mode === "dark" ? theme.colors.text : theme.colors.textMuted}>
252
- <span style={{ bold: theme.mode === "dark" }}>{"Dark"}</span>
253
- </text>
254
- </box>
255
- <box flexDirection="row">
256
- <text fg={theme.mode === "light" && tab() === "mode" ? theme.colors.primary : theme.colors.textMuted}>
257
- {theme.mode === "light" && tab() === "mode" ? "❯ " : " "}
258
- </text>
259
- <text fg={theme.mode === "light" ? theme.colors.text : theme.colors.textMuted}>
260
- <span style={{ bold: theme.mode === "light" }}>{"Light"}</span>
261
- </text>
255
+ <box flexDirection="column" paddingLeft={2}>
256
+ <box flexDirection="row">
257
+ <text fg={c().primary}><span style={{ bold: true }}>{"You: "}</span></text>
258
+ <text fg={c().text}>{"Show me trending posts"}</text>
259
+ </box>
260
+ <box flexDirection="row">
261
+ <text fg={c().accent}><span style={{ bold: true }}>{"AI: "}</span></text>
262
+ <text fg={c().text}>{"Here are today's top..."}</text>
263
+ </box>
264
+ <box height={1} />
265
+ <box flexDirection="row">
266
+ <text fg={c().success}>{"✓ "}</text>
267
+ <text fg={c().text}>{"Published successfully"}</text>
268
+ </box>
269
+ <box flexDirection="row">
270
+ <text fg={c().warning}>{"⚠ "}</text>
271
+ <text fg={c().textMuted}>{"Rate limit reached"}</text>
272
+ </box>
273
+ <box flexDirection="row">
274
+ <text fg={c().error}>{"✗ "}</text>
275
+ <text fg={c().textMuted}>{"Connection failed"}</text>
276
+ </box>
262
277
  </box>
263
278
  </box>
264
279
  </box>