spinuptui 0.24.2 → 0.24.4

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/CHANGELOG.md CHANGED
@@ -11,6 +11,41 @@ versions; such changes are called out here.
11
11
 
12
12
  ## [Unreleased]
13
13
 
14
+ ## [0.24.4] - 2026-08-06
15
+
16
+ ### Fixed
17
+ - **The `Unknown component type: toaster` startup crash is now actually
18
+ fixed for real installs**, not just local project installs. `0.24.3`'s
19
+ `package.json` `overrides` only take effect when this repo's own
20
+ `package.json` is the root of the install — they're silently ignored when
21
+ spinuptui is installed as a dependency of something else, which is exactly
22
+ what a global `bun add -g` / `npm install -g` does, so the crash persisted
23
+ for every real user. Root cause traces back to the toast library
24
+ (`@opentui-ui/toast`) itself: it's stuck on a peer-dependency range from
25
+ opentui's `0.1.x` days, and no version pin on our side can bind how a
26
+ downstream install resolves *its* transitive dependencies. Every call site
27
+ in this app only ever fires a single-line success message, so it's
28
+ replaced with a small first-party toast implementation (`src/ui/toast.ts`
29
+ + `src/ui/ToastStack.tsx`) built directly on the `@opentui/core`/
30
+ `@opentui/react` we already depend on — no external package, no peer-
31
+ dependency surface for a future version bump to break again.
32
+
33
+ ## [0.24.3] - 2026-08-06
34
+
35
+ ### Fixed
36
+ - **Crash on startup: `Unknown component type: toaster`.** The `0.24.2` pin
37
+ fixed the toast-dismissal crash but exposed a second issue: the toast
38
+ library declares an ancient peer-dependency range on `@opentui/core`/
39
+ `@opentui/react` (`^0.1.63`, predating their current `0.4.x` releases)
40
+ that our pinned version doesn't satisfy. Because that range goes
41
+ unsatisfied, the package manager installed a second, independent nested
42
+ copy of `@opentui/react` under the toast library instead of reusing ours
43
+ — and since the toast library registers its custom `<toaster>` component
44
+ on that nested copy's module-local registry, our app's copy never saw it
45
+ and crashed the first time it tried to render one. Added `overrides` in
46
+ `package.json` to force every dependency in the tree onto the same single
47
+ `@opentui/core`/`@opentui/react` install, closing the gap.
48
+
14
49
  ## [0.24.2] - 2026-08-06
15
50
 
16
51
  ### Fixed
@@ -1264,7 +1299,9 @@ Initial tagged release.
1264
1299
  ### Notes
1265
1300
  - Read-only release: works with a SpinupWP **Read Only** API token.
1266
1301
 
1267
- [Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.24.2...HEAD
1302
+ [Unreleased]: https://github.com/mwender/spinupwp-tui/compare/v0.24.4...HEAD
1303
+ [0.24.4]: https://github.com/mwender/spinupwp-tui/compare/v0.24.3...v0.24.4
1304
+ [0.24.3]: https://github.com/mwender/spinupwp-tui/compare/v0.24.2...v0.24.3
1268
1305
  [0.24.2]: https://github.com/mwender/spinupwp-tui/compare/v0.24.1...v0.24.2
1269
1306
  [0.24.1]: https://github.com/mwender/spinupwp-tui/compare/v0.24.0...v0.24.1
1270
1307
  [0.24.0]: https://github.com/mwender/spinupwp-tui/compare/v0.23.0...v0.24.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "spinuptui",
3
- "version": "0.24.2",
3
+ "version": "0.24.4",
4
4
  "description": "SpinupTUI — a terminal dashboard for browsing, monitoring, and managing your SpinupWP servers and sites.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -41,9 +41,8 @@
41
41
  "author": "Michael Wender",
42
42
  "license": "MIT",
43
43
  "dependencies": {
44
- "@opentui-ui/toast": "^0.0.5",
45
- "@opentui/core": "0.4.2",
46
- "@opentui/react": "0.4.2",
44
+ "@opentui/core": "^0.4.1",
45
+ "@opentui/react": "^0.4.1",
47
46
  "react": "^19.0.0",
48
47
  "socket.io-client": "^4.8.3"
49
48
  },
package/src/ui/App.tsx CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { useEffect, useState } from "react"
4
4
  import { useKeyboard, useRenderer, useTerminalDimensions } from "@opentui/react"
5
- import { Toaster } from "@opentui-ui/toast/react"
5
+ import { ToastStack } from "./ToastStack.tsx"
6
6
  import { theme } from "../lib/theme.ts"
7
7
  import { useStore } from "./store.tsx"
8
8
  import { Splash } from "./Splash.tsx"
@@ -262,10 +262,7 @@ export function App() {
262
262
  {store.connectZoneTarget && <ProviderConnect />}
263
263
  {store.dnsRecordsTarget && <DnsRecords />}
264
264
  {store.kumaSite && <KumaSite />}
265
- {/* Async-completion toasts (PHP upgrade, server reboot/restart). Mounted last
266
- so it draws over every view + overlay; top-right, nudged clear of the
267
- 2-row Header. It never takes keyboard focus. */}
268
- <Toaster position="top-right" offset={{ top: 2, right: 2 }} />
265
+ <ToastStack />
269
266
  </box>
270
267
  )
271
268
  }
@@ -0,0 +1,33 @@
1
+ // Renders the toast queue from ./toast.ts. See that file for why this is
2
+ // first-party instead of a dependency. Mounted last in App.tsx so it draws
3
+ // over every view + overlay; top-right, nudged clear of the 2-row Header.
4
+ // It never takes keyboard focus.
5
+
6
+ import { useSyncExternalStore } from "react"
7
+ import { theme } from "../lib/theme.ts"
8
+ import { getToasts, subscribeToasts } from "./toast.ts"
9
+
10
+ const TOAST_WIDTH = 60
11
+
12
+ export function ToastStack() {
13
+ const toasts = useSyncExternalStore(subscribeToasts, getToasts, getToasts)
14
+
15
+ if (toasts.length === 0) return null
16
+
17
+ return (
18
+ <box style={{ position: "absolute", top: 2, right: 2, flexDirection: "column", gap: 1, zIndex: 300 }}>
19
+ {toasts.map((t) => (
20
+ <box
21
+ key={t.id}
22
+ border
23
+ borderColor={theme.good}
24
+ backgroundColor={theme.bgPanel}
25
+ style={{ flexDirection: "row", gap: 1, maxWidth: TOAST_WIDTH, paddingLeft: 1, paddingRight: 1 }}
26
+ >
27
+ <text content="✓" fg={theme.good} />
28
+ <text content={t.message} fg={theme.text} />
29
+ </box>
30
+ ))}
31
+ </box>
32
+ )
33
+ }
package/src/ui/store.tsx CHANGED
@@ -6,7 +6,7 @@
6
6
  // trigger a refresh.
7
7
 
8
8
  import { createContext, useContext, useCallback, useEffect, useMemo, useRef, useState, type ReactNode } from "react"
9
- import { toast } from "@opentui-ui/toast"
9
+ import { toast } from "./toast.ts"
10
10
  import { SpinupWPClient, ApiError, type ServerService, type SpinupWPClientLike } from "../api/client.ts"
11
11
  import { isDevMode } from "../dev/devMode.ts"
12
12
  import { createMockClient } from "../dev/mockClient.ts"
@@ -0,0 +1,49 @@
1
+ // Minimal first-party toast notifications.
2
+ //
3
+ // Replaces the @opentui-ui/toast package, which is stuck on a peer-dependency
4
+ // range from opentui's 0.1.x days and breaks against the @opentui/core 0.4.x
5
+ // line we're on (see CHANGELOG v0.24.2/v0.24.3). Every call site in this app
6
+ // only ever fires a single-line success message, so a small first-party
7
+ // implementation is simpler — and immune to upstream drift — than depending
8
+ // on a package built for a much larger feature set we don't use.
9
+ //
10
+ // A plain module-level subscriber list (rather than React state) so callers
11
+ // outside the component tree — store.tsx's async completion handlers — can
12
+ // fire a toast without needing a ref or context. ToastStack.tsx reads this
13
+ // via useSyncExternalStore.
14
+
15
+ export interface ToastItem {
16
+ id: number
17
+ message: string
18
+ }
19
+
20
+ const TOAST_DURATION_MS = 4000
21
+
22
+ let nextId = 1
23
+ let toasts: ToastItem[] = []
24
+ const listeners = new Set<() => void>()
25
+
26
+ function notify() {
27
+ for (const listener of listeners) listener()
28
+ }
29
+
30
+ export function subscribeToasts(listener: () => void): () => void {
31
+ listeners.add(listener)
32
+ return () => listeners.delete(listener)
33
+ }
34
+
35
+ export function getToasts(): ToastItem[] {
36
+ return toasts
37
+ }
38
+
39
+ export const toast = {
40
+ success(message: string) {
41
+ const id = nextId++
42
+ toasts = [...toasts, { id, message }]
43
+ notify()
44
+ setTimeout(() => {
45
+ toasts = toasts.filter((t) => t.id !== id)
46
+ notify()
47
+ }, TOAST_DURATION_MS)
48
+ },
49
+ }