@mp-lb/termkit 0.2.2 → 0.2.3

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.
Files changed (74) hide show
  1. package/README.md +18 -111
  2. package/dist/Terminal.d.ts +7 -1
  3. package/dist/Terminal.d.ts.map +1 -1
  4. package/dist/Terminal.js +87 -14
  5. package/dist/TermkitContext.d.ts +8 -0
  6. package/dist/TermkitContext.d.ts.map +1 -0
  7. package/dist/TermkitContext.js +2 -0
  8. package/dist/TermkitProvider.d.ts +8 -0
  9. package/dist/TermkitProvider.d.ts.map +1 -0
  10. package/dist/{context.js → TermkitProvider.js} +11 -18
  11. package/dist/createTerminalWriteQueue.d.ts.map +1 -1
  12. package/dist/createTerminalWriteQueue.js +1 -2
  13. package/dist/{store.d.ts → createTermkitStore.d.ts} +1 -1
  14. package/dist/createTermkitStore.d.ts.map +1 -0
  15. package/dist/{store.js → createTermkitStore.js} +11 -5
  16. package/dist/fitTerminal.d.ts.map +1 -1
  17. package/dist/fitTerminal.js +35 -3
  18. package/dist/index.d.ts +16 -5
  19. package/dist/index.d.ts.map +1 -1
  20. package/dist/index.js +12 -3
  21. package/dist/terminalColors.d.ts +11 -0
  22. package/dist/terminalColors.d.ts.map +1 -0
  23. package/dist/terminalColors.js +57 -0
  24. package/dist/types.d.ts +4 -0
  25. package/dist/types.d.ts.map +1 -1
  26. package/dist/types.js +0 -1
  27. package/dist/useDanglingSessions.d.ts +3 -0
  28. package/dist/useDanglingSessions.d.ts.map +1 -0
  29. package/dist/useDanglingSessions.js +6 -0
  30. package/dist/useFocusedSession.d.ts +2 -0
  31. package/dist/useFocusedSession.d.ts.map +1 -0
  32. package/dist/useFocusedSession.js +6 -0
  33. package/dist/useIsFocused.d.ts +2 -0
  34. package/dist/useIsFocused.d.ts.map +1 -0
  35. package/dist/useIsFocused.js +4 -0
  36. package/dist/useIsRinging.d.ts +2 -0
  37. package/dist/useIsRinging.d.ts.map +1 -0
  38. package/dist/useIsRinging.js +4 -0
  39. package/dist/useRingingSessions.d.ts +2 -0
  40. package/dist/useRingingSessions.d.ts.map +1 -0
  41. package/dist/useRingingSessions.js +6 -0
  42. package/dist/useSession.d.ts +3 -0
  43. package/dist/useSession.d.ts.map +1 -0
  44. package/dist/useSession.js +6 -0
  45. package/dist/useSessionStatus.d.ts +3 -0
  46. package/dist/useSessionStatus.d.ts.map +1 -0
  47. package/dist/useSessionStatus.js +6 -0
  48. package/dist/useSessions.d.ts +3 -0
  49. package/dist/useSessions.d.ts.map +1 -0
  50. package/dist/useSessions.js +6 -0
  51. package/dist/useSlots.d.ts +3 -0
  52. package/dist/useSlots.d.ts.map +1 -0
  53. package/dist/useSlots.js +6 -0
  54. package/dist/useTermkitActions.d.ts +10 -0
  55. package/dist/useTermkitActions.d.ts.map +1 -0
  56. package/dist/useTermkitActions.js +20 -0
  57. package/dist/useTermkitContext.d.ts +3 -0
  58. package/dist/useTermkitContext.d.ts.map +1 -0
  59. package/dist/useTermkitContext.js +8 -0
  60. package/package.json +7 -7
  61. package/dist/Terminal.js.map +0 -1
  62. package/dist/context.d.ts +0 -15
  63. package/dist/context.d.ts.map +0 -1
  64. package/dist/context.js.map +0 -1
  65. package/dist/createTerminalWriteQueue.js.map +0 -1
  66. package/dist/fitTerminal.js.map +0 -1
  67. package/dist/hooks.d.ts +0 -22
  68. package/dist/hooks.d.ts.map +0 -1
  69. package/dist/hooks.js +0 -83
  70. package/dist/hooks.js.map +0 -1
  71. package/dist/index.js.map +0 -1
  72. package/dist/store.d.ts.map +0 -1
  73. package/dist/store.js.map +0 -1
  74. package/dist/types.js.map +0 -1
package/README.md CHANGED
@@ -1,122 +1,29 @@
1
1
  # @mp-lb/termkit
2
2
 
3
- Easily orchestrate terminal sessions in React. Render a `<Terminal />`, and manage
4
- all your live sessions — including their attention bells — with a small set of
5
- global hooks.
6
-
7
- ## Mental model
8
-
9
- - A **session** is a long-lived shell (a PTY) living in the **daemon**
10
- (`@mp-lb/termkit-server`), addressed by a stable id you choose.
11
- - A `<Terminal session={id} />` is just a **view** onto a session. Mounting it
12
- attaches (reconnecting to the existing shell if there is one, via a replayed
13
- screen snapshot); unmounting detaches but leaves the shell running.
14
- - The `<TermkitProvider>` holds the **global registry** of sessions and polls the
15
- daemon for the live list. So there is **no one-to-one** requirement between the
16
- terminals on screen and the sessions that are alive — you can have background
17
- sessions with no terminal, and you can see/manage them from anywhere.
18
-
19
- ## Two ways to use a session
20
-
21
- - **Local:** drop `<Terminal session="abc" />` somewhere. Rendering it creates or
22
- reattaches session `abc`. Its lifecycle reads as component-local — but the
23
- session itself outlives the component.
24
- - **Global:** use the hooks (`useSessions`, `useRingingSessions`,
25
- `useTermkitActions`, …) anywhere under the provider to list every session, watch
26
- which ones are **belling** for attention, and act on them — whether or not a
27
- terminal is mounted for them.
28
-
29
- ## Setup
3
+ Build React terminal experiences backed by persistent PTY sessions. This workspace package is private while its API is being designed; use it through the workspace dependency `workspace:*`.
30
4
 
31
5
  ```tsx
32
- import { TermkitProvider } from "@mp-lb/termkit"
6
+ import { Terminal, TermkitProvider, type TermkitConfig } from "@mp-lb/termkit";
33
7
 
34
- const config = {
35
- // Where the byte stream lives. `params` is forwarded to the daemon as session
36
- // metadata and drives its resolveCwd.
8
+ const config: TermkitConfig = {
37
9
  connectionUrl: ({ sessionId, params }) =>
38
- `ws://${location.host}/_terminal?` +
39
- new URLSearchParams({ session: sessionId, ...params }),
40
- // The control plane: list / kill / clear-bell. Wire to your backend.
10
+ `wss://example.com/_terminal?${new URLSearchParams({ ...params, session: sessionId })}`,
41
11
  transport: {
42
- list: () => trpc.terminal.list.query(),
43
- kill: (sessionId) => trpc.terminal.kill.mutate({ sessionId }),
44
- seen: (sessionId) => trpc.terminal.seen.mutate({ sessionId }),
12
+ list: () => api.terminal.list(),
13
+ kill: (sessionId) => api.terminal.kill(sessionId),
14
+ seen: (sessionId) => api.terminal.seen(sessionId),
45
15
  },
46
- }
47
-
48
- <TermkitProvider config={config}>
49
- <App />
50
- </TermkitProvider>
51
- ```
52
-
53
- ## Rendering a terminal
54
-
55
- ```tsx
56
- import { Terminal } from "@mp-lb/termkit"
57
-
58
- <Terminal session="global" autoFocus />
59
- <Terminal session={tab.id} params={{ slug: "myproject" }} />
60
- ```
61
-
62
- Props: `session` (required, stable id), `params`, `autoFocus`, `dismissOnView`
63
- (default `true` — clear this session's bell when you view it), `pendingCommand`
64
- (auto-typed once after the prompt settles), `onExitFocus` (called on
65
- Ctrl+Shift+Enter, after termkit blurs the terminal), `background`, `className`,
66
- `style`.
67
-
68
- ## Bells & session hooks
69
-
70
- ```tsx
71
- const sessions = useSessions() // LiveSession[] — the global list
72
- const ringing = useRingingSessions() // Set<string> of ids belling for attention
73
- const isRinging = useIsRinging(id) // one session
74
- const status = useSessionStatus(id) // connecting | ready | disconnected | dead
75
- const { focus, kill, dismissBell, dismissAllBells, setPendingCommand } =
76
- useTermkitActions()
16
+ };
17
+
18
+ export const Shell = () => (
19
+ <TermkitProvider config={config}>
20
+ <div style={{ height: 400 }}>
21
+ <Terminal session="workspace" autoFocus />
22
+ </div>
23
+ </TermkitProvider>
24
+ );
77
25
  ```
78
26
 
79
- Bells clear automatically when you view a session (`dismissOnView`), or explicitly
80
- via `dismissBell(id)` / `dismissAllBells()`.
81
-
82
- ## Theming
83
-
84
- `<Terminal>` renders xterm on a transparent surface inside a full-size wrapper.
85
- Set the wrapper background with Tailwind/classes, an explicit colour, a CSS
86
- variable, or a provider default:
87
-
88
- ```tsx
89
- <Terminal session="global" className="bg-zinc-950" />
90
- <Terminal session="global" background="var(--background)" />
91
- <Terminal session="global" style={{ backgroundColor: theme.terminal }} />
92
-
93
- const config = {
94
- ...baseConfig,
95
- terminalOptions: { background: "var(--termkit-bg)" },
96
- }
97
- ```
98
-
99
- Because the terminal itself is transparent, any leftover space that cannot fit a
100
- full row or column uses the same background as the terminal.
101
-
102
- ## Migrating dash onto termkit
103
-
104
- termkit is a superset of what `dash` does today; the mapping is mechanical:
105
-
106
- | dash | termkit |
107
- | --- | --- |
108
- | `<Terminal slot=… slug=… instanceId=… />` | `<Terminal session=… params={{ slug, instanceId }} />` |
109
- | `useTerminalStore` (zustand) | the provider store + `useTermkitActions` |
110
- | `live-sessions.ts` poller + `useLiveSessions` | built into `<TermkitProvider>` + `useSessions` |
111
- | `useRingingSessionIds` | `useRingingSessions` |
112
- | `markTerminalSeen` on focus/view | `dismissOnView` (default on) + `dismissBell` |
113
- | `LiveSession.slug` / `.instanceId` | `LiveSession.meta.slug` / `.meta.instanceId` |
114
- | exit-chord → focus `[data-session-tab]` | `onExitFocus` callback |
115
- | `useKnownSessionIds` / `useSessionSummaries` | **stay in dash** — built on `useSessions()` (project/slug grouping is dash domain) |
116
- | daemon (`server/terminal-daemon.ts`) | `createTerminalDaemon({ resolveCwd })` from `@mp-lb/termkit-server` |
117
- | tRPC `terminal` router | `createTerminalRouter(client)` from `@mp-lb/termkit-trpc`, or keep dash's |
27
+ `api` represents your host application's control-plane adapter. Keep the provider configuration stable. Unmounting a terminal detaches the view; the shell keeps running in the daemon. Remounting the same session ID reattaches to its current screen.
118
28
 
119
- The dash-specific seams — the `~/Code` working-directory resolution and the
120
- project/instance groupings — are deliberately **not** baked in: the daemon takes a
121
- `resolveCwd` callback, and per-project rollups stay in dash, layered over the
122
- generic `useSessions()` list.
29
+ See the [guide](../../docs/public/index.mdx) and [generated reference](../../docs/public/api/index.mdx). The Node daemon and optional tRPC adapter live in sibling packages and are kept out of the browser entrypoint.
@@ -1,5 +1,7 @@
1
1
  import { type CSSProperties } from "react";
2
+ import { type ITheme } from "@xterm/xterm";
2
3
  import "@xterm/xterm/css/xterm.css";
4
+ import { type TerminalColors } from "./terminalColors.js";
3
5
  export interface TerminalProps {
4
6
  session: string;
5
7
  params?: Record<string, string>;
@@ -7,9 +9,13 @@ export interface TerminalProps {
7
9
  dismissOnView?: boolean;
8
10
  pendingCommand?: string;
9
11
  onExitFocus?: () => void;
12
+ exitFocusChord?: "ctrl-enter" | "ctrl-shift-enter";
10
13
  background?: string;
14
+ colors?: TerminalColors;
15
+ theme?: ITheme;
16
+ visible?: boolean;
11
17
  className?: string;
12
18
  style?: CSSProperties;
13
19
  }
14
- export declare function Terminal({ session, params, autoFocus, dismissOnView, pendingCommand, onExitFocus, background, className, style, }: TerminalProps): import("react/jsx-runtime").JSX.Element;
20
+ export declare function Terminal({ session, params, autoFocus, dismissOnView, pendingCommand, onExitFocus, exitFocusChord, background, colors, theme, visible, className, style, }: TerminalProps): import("react").JSX.Element;
15
21
  //# sourceMappingURL=Terminal.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Terminal.d.ts","sourceRoot":"","sources":["../src/Terminal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAA;AAG7D,OAAO,4BAA4B,CAAA;AAWnC,MAAM,WAAW,aAAa;IAK5B,OAAO,EAAE,MAAM,CAAA;IAIf,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAA;IAE/B,SAAS,CAAC,EAAE,OAAO,CAAA;IAKnB,aAAa,CAAC,EAAE,OAAO,CAAA;IAGvB,cAAc,CAAC,EAAE,MAAM,CAAA;IAIvB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAA;IAIxB,UAAU,CAAC,EAAE,MAAM,CAAA;IAGnB,SAAS,CAAC,EAAE,MAAM,CAAA;IAClB,KAAK,CAAC,EAAE,aAAa,CAAA;CACtB;AAED,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EACP,MAAM,EACN,SAAiB,EACjB,aAAoB,EACpB,cAAc,EACd,WAAW,EACX,UAAU,EACV,SAAS,EACT,KAAK,GACN,EAAE,aAAa,2CA2Sf"}
1
+ {"version":3,"file":"Terminal.d.ts","sourceRoot":"","sources":["../src/Terminal.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,aAAa,EAAE,MAAM,OAAO,CAAC;AAC9D,OAAO,EAAqB,KAAK,MAAM,EAAE,MAAM,cAAc,CAAC;AAE9D,OAAO,4BAA4B,CAAC;AAUpC,OAAO,EAEL,KAAK,cAAc,EACpB,MAAM,qBAAqB,CAAC;AAE7B,MAAM,WAAW,aAAa;IAK5B,OAAO,EAAE,MAAM,CAAC;IAIhB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAEhC,SAAS,CAAC,EAAE,OAAO,CAAC;IAKpB,aAAa,CAAC,EAAE,OAAO,CAAC;IAGxB,cAAc,CAAC,EAAE,MAAM,CAAC;IAIxB,WAAW,CAAC,EAAE,MAAM,IAAI,CAAC;IAGzB,cAAc,CAAC,EAAE,YAAY,GAAG,kBAAkB,CAAC;IAInD,UAAU,CAAC,EAAE,MAAM,CAAC;IAEpB,MAAM,CAAC,EAAE,cAAc,CAAC;IAIxB,KAAK,CAAC,EAAE,MAAM,CAAC;IAGf,OAAO,CAAC,EAAE,OAAO,CAAC;IAGlB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,KAAK,CAAC,EAAE,aAAa,CAAC;CACvB;AAED,wBAAgB,QAAQ,CAAC,EACvB,OAAO,EACP,MAAM,EACN,SAAiB,EACjB,aAAoB,EACpB,cAAc,EACd,WAAW,EACX,cAAmC,EACnC,UAAU,EACV,MAAM,EACN,KAAK,EACL,OAAc,EACd,SAAS,EACT,KAAK,GACN,EAAE,aAAa,+BAscf"}
package/dist/Terminal.js CHANGED
@@ -4,12 +4,14 @@ import { Terminal as XTerm } from "@xterm/xterm";
4
4
  import { FitAddon } from "@xterm/addon-fit";
5
5
  import "@xterm/xterm/css/xterm.css";
6
6
  import { CLOSE_SESSION_GONE, HEARTBEAT_INTERVAL_MS, HEARTBEAT_PONG_GRACE_MS, } from "@mp-lb/termkit-core";
7
- import { useTermkitContext } from "./context.js";
7
+ import { useTermkitContext } from "./useTermkitContext.js";
8
8
  import { createTerminalWriteQueue } from "./createTerminalWriteQueue.js";
9
9
  import { fitTerminal } from "./fitTerminal.js";
10
- export function Terminal({ session, params, autoFocus = false, dismissOnView = true, pendingCommand, onExitFocus, background, className, style, }) {
10
+ import { resolveTerminalColors, } from "./terminalColors.js";
11
+ export function Terminal({ session, params, autoFocus = false, dismissOnView = true, pendingCommand, onExitFocus, exitFocusChord = "ctrl-shift-enter", background, colors, theme, visible = true, className, style, }) {
11
12
  const { store, config } = useTermkitContext();
12
13
  const containerRef = useRef(null);
14
+ const refreshLayoutBurstRef = useRef(() => { });
13
15
  const containerStyle = {
14
16
  width: "100%",
15
17
  height: "100%",
@@ -18,7 +20,7 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
18
20
  };
19
21
  useEffect(() => {
20
22
  const sessionId = session;
21
- const { attach, detach, focus, blur, setStatus, setPendingCommand, consumePendingCommand } = store.getState();
23
+ const { attach, detach, focus, blur, setStatus, setPendingCommand, consumePendingCommand, } = store.getState();
22
24
  attach(sessionId);
23
25
  if (pendingCommand !== undefined)
24
26
  setPendingCommand(sessionId, pendingCommand);
@@ -32,15 +34,38 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
32
34
  // if the ref is repointed before teardown runs.
33
35
  const container = containerRef.current;
34
36
  const opts = config.terminalOptions ?? {};
37
+ const terminalTheme = {
38
+ ...resolveTerminalColors(opts.colors),
39
+ ...opts.theme,
40
+ ...resolveTerminalColors(colors),
41
+ ...theme,
42
+ background: theme?.background ??
43
+ (typeof colors === "object" ? colors.background : undefined) ??
44
+ opts.theme?.background ??
45
+ (typeof opts.colors === "object"
46
+ ? opts.colors.background
47
+ : undefined) ??
48
+ background ??
49
+ opts.background,
50
+ };
35
51
  const term = new XTerm({
36
52
  fontFamily: opts.fontFamily ?? '"IBM Plex Mono", monospace',
37
53
  fontSize: opts.fontSize ?? 13,
38
54
  cursorBlink: opts.cursorBlink ?? true,
39
- theme: { background: "transparent" },
55
+ theme: terminalTheme,
40
56
  });
41
57
  const fitAddon = new FitAddon();
42
58
  term.loadAddon(fitAddon);
43
59
  term.open(container);
60
+ for (const selector of [
61
+ ".xterm-viewport",
62
+ ".xterm-scrollable-element",
63
+ ".xterm-screen",
64
+ ]) {
65
+ container
66
+ .querySelector(selector)
67
+ ?.style.setProperty("background-color", "transparent", "important");
68
+ }
44
69
  let lastFitSize = fitTerminal(container, term, fitAddon, null);
45
70
  let lastSentSize = null;
46
71
  // If a bell lands while you're actively looking at this session — its terminal
@@ -58,6 +83,7 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
58
83
  else if (term.textarea) {
59
84
  term.textarea.tabIndex = -1;
60
85
  }
86
+ let scheduleLayoutRefreshBurst = () => { };
61
87
  // Subscribe to store focus changes so external callers (focus/blur actions) work.
62
88
  const unsubFocus = store.subscribe((state, prev) => {
63
89
  const isFocused = state.focusedSlot === sessionId;
@@ -67,6 +93,7 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
67
93
  if (term.textarea)
68
94
  term.textarea.tabIndex = 0;
69
95
  term.focus();
96
+ scheduleLayoutRefreshBurst();
70
97
  }
71
98
  else {
72
99
  term.blur();
@@ -79,15 +106,21 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
79
106
  const onFocus = () => {
80
107
  focus(sessionId);
81
108
  markSeen();
109
+ scheduleLayoutRefreshBurst();
82
110
  };
83
111
  const onBlur = () => blur(sessionId);
84
112
  term.textarea?.addEventListener("focus", onFocus);
85
113
  term.textarea?.addEventListener("blur", onBlur);
86
- // Ctrl+Shift+Enter exits terminal focus. Handled here (capture on the container,
114
+ // The configured chord exits terminal focus. Handled here (capture on the container,
87
115
  // before xterm sees it) because global capture listeners don't reliably fire for
88
116
  // Enter when xterm has focus. We blur and hand control back to the host.
89
117
  const onExitChord = (e) => {
90
- if (e.key === "Enter" && e.shiftKey && e.ctrlKey) {
118
+ const isExitChord = e.key === "Enter" &&
119
+ e.ctrlKey &&
120
+ !e.altKey &&
121
+ !e.metaKey &&
122
+ (exitFocusChord === "ctrl-shift-enter" ? e.shiftKey : !e.shiftKey);
123
+ if (isExitChord) {
91
124
  e.preventDefault();
92
125
  e.stopPropagation();
93
126
  blur(sessionId);
@@ -103,6 +136,7 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
103
136
  let socket = null;
104
137
  let reconnectTimer = null;
105
138
  let refreshFrame = null;
139
+ const refreshTimers = new Set();
106
140
  let disposed = false;
107
141
  let attachedBefore = false;
108
142
  // App-level heartbeat. A half-open socket (laptop sleep/wake, Wi-Fi change, a
@@ -137,7 +171,9 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
137
171
  try {
138
172
  socket?.close();
139
173
  }
140
- catch { /* already closing */ }
174
+ catch {
175
+ /* already closing */
176
+ }
141
177
  }, HEARTBEAT_PONG_GRACE_MS);
142
178
  }
143
179
  }, HEARTBEAT_INTERVAL_MS);
@@ -197,7 +233,10 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
197
233
  if (nextSize === null)
198
234
  return;
199
235
  lastFitSize = nextSize;
200
- if (lastSentSize?.cols === nextSize.cols && lastSentSize.rows === nextSize.rows)
236
+ if (lastSentSize?.cols === nextSize.cols &&
237
+ lastSentSize.rows === nextSize.rows)
238
+ return;
239
+ if (socket?.readyState !== WebSocket.OPEN)
201
240
  return;
202
241
  lastSentSize = nextSize;
203
242
  send({ type: "resize", cols: nextSize.cols, rows: nextSize.rows });
@@ -215,9 +254,26 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
215
254
  return;
216
255
  refreshFrame = requestAnimationFrame(refreshVisibleRows);
217
256
  };
257
+ const refreshAfterDelay = (ms) => {
258
+ const timer = setTimeout(() => {
259
+ refreshTimers.delete(timer);
260
+ refreshAfterLayout();
261
+ }, ms);
262
+ refreshTimers.add(timer);
263
+ };
264
+ const refreshLayoutBurst = () => {
265
+ refreshAfterLayout();
266
+ refreshAfterDelay(50);
267
+ refreshAfterDelay(150);
268
+ refreshAfterDelay(500);
269
+ refreshAfterDelay(1000);
270
+ refreshAfterDelay(2000);
271
+ };
272
+ scheduleLayoutRefreshBurst = refreshLayoutBurst;
273
+ refreshLayoutBurstRef.current = refreshLayoutBurst;
218
274
  const refreshWhenVisible = () => {
219
275
  if (document.visibilityState === "visible")
220
- refreshAfterLayout();
276
+ refreshLayoutBurst();
221
277
  };
222
278
  const terminalWriteQueue = createTerminalWriteQueue(term, refreshAfterLayout);
223
279
  const connect = () => {
@@ -240,6 +296,7 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
240
296
  attachedBefore = true;
241
297
  setStatus(sessionId, "ready");
242
298
  fitAndSendResize();
299
+ refreshLayoutBurst();
243
300
  // Run the queued command once, on the very first attach. Wait for the prompt
244
301
  // to finish painting (see noteOutput/flushPendingCmd) so it lands on the real
245
302
  // prompt instead of racing shell startup. The cap timer is a backstop.
@@ -262,7 +319,9 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
262
319
  if (msg.type === "pong")
263
320
  onPong();
264
321
  }
265
- catch { /* unknown control frame — ignore */ }
322
+ catch {
323
+ /* unknown control frame — ignore */
324
+ }
266
325
  }
267
326
  return;
268
327
  }
@@ -288,20 +347,30 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
288
347
  };
289
348
  };
290
349
  connect();
291
- term.onData((data) => send({ type: "input", data }));
350
+ term.onData((data) => {
351
+ fitAndSendResize();
352
+ send({ type: "input", data });
353
+ });
292
354
  const observer = new ResizeObserver(refreshAfterLayout);
293
355
  observer.observe(container);
294
356
  container.addEventListener("pointerdown", refreshAfterLayout, true);
295
- container.addEventListener("wheel", refreshAfterLayout, { capture: true, passive: true });
357
+ container.addEventListener("wheel", refreshAfterLayout, {
358
+ capture: true,
359
+ passive: true,
360
+ });
296
361
  window.addEventListener("focus", refreshAfterLayout);
297
362
  window.addEventListener("pageshow", refreshAfterLayout);
298
363
  document.addEventListener("visibilitychange", refreshWhenVisible);
364
+ document.fonts?.ready.then(refreshLayoutBurst).catch(() => { });
299
365
  return () => {
300
366
  disposed = true;
301
367
  if (reconnectTimer !== null)
302
368
  clearTimeout(reconnectTimer);
303
369
  if (refreshFrame !== null)
304
370
  cancelAnimationFrame(refreshFrame);
371
+ for (const timer of refreshTimers)
372
+ clearTimeout(timer);
373
+ refreshTimers.clear();
305
374
  stopHeartbeat();
306
375
  clearAutoType();
307
376
  blur(sessionId);
@@ -323,8 +392,12 @@ export function Terminal({ session, params, autoFocus = false, dismissOnView = t
323
392
  term.textarea?.removeEventListener("blur", onBlur);
324
393
  terminalWriteQueue.dispose();
325
394
  term.dispose();
395
+ refreshLayoutBurstRef.current = () => { };
326
396
  };
327
397
  }, [session]); // eslint-disable-line react-hooks/exhaustive-deps
328
- return _jsx("div", { ref: containerRef, className: className, style: containerStyle });
398
+ useEffect(() => {
399
+ if (visible)
400
+ refreshLayoutBurstRef.current();
401
+ }, [visible]);
402
+ return (_jsx("div", { ref: containerRef, className: className, style: containerStyle }));
329
403
  }
330
- //# sourceMappingURL=Terminal.js.map
@@ -0,0 +1,8 @@
1
+ import type { TermkitStore } from "./createTermkitStore.js";
2
+ import type { TermkitConfig } from "./types.js";
3
+ export interface TermkitContextValue {
4
+ store: TermkitStore;
5
+ config: TermkitConfig;
6
+ }
7
+ export declare const TermkitContext: import("react").Context<TermkitContextValue | null>;
8
+ //# sourceMappingURL=TermkitContext.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TermkitContext.d.ts","sourceRoot":"","sources":["../src/TermkitContext.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAC5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,mBAAmB;IAClC,KAAK,EAAE,YAAY,CAAC;IACpB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,eAAO,MAAM,cAAc,qDAAkD,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { createContext } from "react";
2
+ export const TermkitContext = createContext(null);
@@ -0,0 +1,8 @@
1
+ import { type ReactNode } from "react";
2
+ import type { TermkitConfig } from "./types.js";
3
+ export interface TermkitProviderProps {
4
+ config: TermkitConfig;
5
+ children: ReactNode;
6
+ }
7
+ export declare const TermkitProvider: ({ config, children }: TermkitProviderProps) => import("react").JSX.Element;
8
+ //# sourceMappingURL=TermkitProvider.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TermkitProvider.d.ts","sourceRoot":"","sources":["../src/TermkitProvider.tsx"],"names":[],"mappings":"AAAA,OAAO,EAAuB,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAG5D,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAEhD,MAAM,WAAW,oBAAoB;IACnC,MAAM,EAAE,aAAa,CAAC;IACtB,QAAQ,EAAE,SAAS,CAAC;CACrB;AAOD,eAAO,MAAM,eAAe,GAAI,sBAAsB,oBAAoB,gCAuCzE,CAAC"}
@@ -1,20 +1,20 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import { createContext, useContext, useEffect, useRef } from "react";
3
- import { createTermkitStore } from "./store.js";
4
- const TermkitContext = createContext(null);
2
+ import { useEffect, useState } from "react";
3
+ import { createTermkitStore } from "./createTermkitStore.js";
4
+ import { TermkitContext } from "./TermkitContext.js";
5
5
  // Holds the global session registry and config for everything below it. Mount it
6
6
  // once near your app root. The registry lives here (not in each <Terminal>), which
7
7
  // is what makes sessions outlive the terminals that render them: a <Terminal> is
8
8
  // just a view onto a session, and the provider's poller keeps the live list fresh
9
9
  // whether or not any terminal is currently mounted for it.
10
- export function TermkitProvider({ config, children }) {
10
+ export const TermkitProvider = ({ config, children }) => {
11
11
  // Create the store once. Config is captured at first render; treat it as stable
12
12
  // (define it outside render or memoize it host-side, like any context value).
13
- const valueRef = useRef(null);
14
- if (valueRef.current === null) {
15
- valueRef.current = { store: createTermkitStore(config.transport), config };
16
- }
17
- const { store } = valueRef.current;
13
+ const [value] = useState(() => ({
14
+ store: createTermkitStore(config.transport),
15
+ config,
16
+ }));
17
+ const { store } = value;
18
18
  // The single poller: one fetch loop publishes the live snapshot every consumer
19
19
  // reads. Cheap on the daemon (an in-memory map read), so a couple of seconds
20
20
  // keeps bells and counts responsive without being chatty.
@@ -39,12 +39,5 @@ export function TermkitProvider({ config, children }) {
39
39
  };
40
40
  // config.transport is captured once with the store; re-poll only if cadence changes.
41
41
  }, [intervalMs, store, config.transport]);
42
- return _jsx(TermkitContext.Provider, { value: valueRef.current, children: children });
43
- }
44
- export function useTermkitContext() {
45
- const ctx = useContext(TermkitContext);
46
- if (!ctx)
47
- throw new Error("useTermkit must be used within a <TermkitProvider>");
48
- return ctx;
49
- }
50
- //# sourceMappingURL=context.js.map
42
+ return (_jsx(TermkitContext.Provider, { value: value, children: children }));
43
+ };
@@ -1 +1 @@
1
- {"version":3,"file":"createTerminalWriteQueue.d.ts","sourceRoot":"","sources":["../src/createTerminalWriteQueue.ts"],"names":[],"mappings":"AAAA,UAAU,gBAAgB;IACxB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAA;CACrD;AAED,eAAO,MAAM,wBAAwB,GAAI,MAAM,gBAAgB,EAAE,aAAa,MAAM,IAAI;gBAmBxE,MAAM;;;CAarB,CAAA"}
1
+ {"version":3,"file":"createTerminalWriteQueue.d.ts","sourceRoot":"","sources":["../src/createTerminalWriteQueue.ts"],"names":[],"mappings":"AAAA,UAAU,gBAAgB;IACxB,KAAK,EAAE,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,CAAC,EAAE,MAAM,IAAI,KAAK,IAAI,CAAC;CACtD;AAED,eAAO,MAAM,wBAAwB,GACnC,MAAM,gBAAgB,EACtB,aAAa,MAAM,IAAI;gBAoBT,MAAM;;;CAarB,CAAC"}
@@ -9,8 +9,8 @@ export const createTerminalWriteQueue = (term, afterWrite) => {
9
9
  chunks = [];
10
10
  isWriting = true;
11
11
  term.write(data, () => {
12
- afterWrite?.();
13
12
  isWriting = false;
13
+ afterWrite?.();
14
14
  drain();
15
15
  });
16
16
  };
@@ -30,4 +30,3 @@ export const createTerminalWriteQueue = (term, afterWrite) => {
30
30
  },
31
31
  };
32
32
  };
33
- //# sourceMappingURL=createTerminalWriteQueue.js.map
@@ -23,4 +23,4 @@ export interface TermkitState {
23
23
  }
24
24
  export type TermkitStore = StoreApi<TermkitState>;
25
25
  export declare function createTermkitStore(transport: TermkitTransport): TermkitStore;
26
- //# sourceMappingURL=store.d.ts.map
26
+ //# sourceMappingURL=createTermkitStore.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"createTermkitStore.d.ts","sourceRoot":"","sources":["../src/createTermkitStore.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,QAAQ,EAAE,MAAM,iBAAiB,CAAC;AAC7D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qBAAqB,CAAC;AACvD,OAAO,KAAK,EAAE,aAAa,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC;AAOlE,MAAM,WAAW,YAAY;IAC3B,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,aAAa,CAAC;CACvB;AAED,MAAM,WAAW,YAAY;IAE3B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,YAAY,CAAC,CAAC;IAEpC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAI3B,eAAe,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAIxC,IAAI,EAAE,WAAW,EAAE,CAAC;IAGpB,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,MAAM,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC,SAAS,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,MAAM,EAAE,aAAa,KAAK,IAAI,CAAC;IAG9D,KAAK,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IACnC,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAGlC,iBAAiB,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAChE,qBAAqB,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;IAG5D,IAAI,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,WAAW,EAAE,CAAC,SAAS,EAAE,MAAM,KAAK,IAAI,CAAC;IAGzC,OAAO,EAAE,CAAC,IAAI,EAAE,WAAW,EAAE,KAAK,IAAI,CAAC;CACxC;AAED,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC,YAAY,CAAC,CAAC;AAElD,wBAAgB,kBAAkB,CAAC,SAAS,EAAE,gBAAgB,GAAG,YAAY,CAkG5E"}
@@ -19,7 +19,10 @@ export function createTermkitStore(transport) {
19
19
  return s;
20
20
  const slots = { ...s.slots };
21
21
  delete slots[sessionId];
22
- return { slots, focusedSlot: s.focusedSlot === sessionId ? null : s.focusedSlot };
22
+ return {
23
+ slots,
24
+ focusedSlot: s.focusedSlot === sessionId ? null : s.focusedSlot,
25
+ };
23
26
  });
24
27
  },
25
28
  setStatus(sessionId, status) {
@@ -34,10 +37,14 @@ export function createTermkitStore(transport) {
34
37
  set({ focusedSlot: sessionId });
35
38
  },
36
39
  blur(sessionId) {
37
- set((s) => ({ focusedSlot: s.focusedSlot === sessionId ? null : s.focusedSlot }));
40
+ set((s) => ({
41
+ focusedSlot: s.focusedSlot === sessionId ? null : s.focusedSlot,
42
+ }));
38
43
  },
39
44
  setPendingCommand(sessionId, command) {
40
- set((s) => ({ pendingCommands: { ...s.pendingCommands, [sessionId]: command } }));
45
+ set((s) => ({
46
+ pendingCommands: { ...s.pendingCommands, [sessionId]: command },
47
+ }));
41
48
  },
42
49
  consumePendingCommand(sessionId) {
43
50
  const command = get().pendingCommands[sessionId];
@@ -74,7 +81,7 @@ export function createTermkitStore(transport) {
74
81
  // Optimistically clear attention so the badge goes away immediately; the
75
82
  // next poll confirms it.
76
83
  set((s) => ({
77
- live: s.live.map((l) => (l.sessionId === sessionId ? { ...l, attention: false } : l)),
84
+ live: s.live.map((l) => l.sessionId === sessionId ? { ...l, attention: false } : l),
78
85
  }));
79
86
  },
80
87
  setLive(live) {
@@ -82,4 +89,3 @@ export function createTermkitStore(transport) {
82
89
  },
83
90
  }));
84
91
  }
85
- //# sourceMappingURL=store.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fitTerminal.d.ts","sourceRoot":"","sources":["../src/fitTerminal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAA;AAChD,OAAO,KAAK,EAAE,QAAQ,IAAI,KAAK,EAAE,MAAM,cAAc,CAAA;AAErD,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAA;IACZ,IAAI,EAAE,MAAM,CAAA;CACb;AAED,eAAO,MAAM,WAAW,GACtB,WAAW,WAAW,EACtB,MAAM,KAAK,EACX,UAAU,QAAQ,EAClB,UAAU,YAAY,GAAG,IAAI,KAC5B,YAAY,GAAG,IASjB,CAAA"}
1
+ {"version":3,"file":"fitTerminal.d.ts","sourceRoot":"","sources":["../src/fitTerminal.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AACjD,OAAO,KAAK,EAAE,QAAQ,IAAI,KAAK,EAAE,MAAM,cAAc,CAAC;AAEtD,UAAU,YAAY;IACpB,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;CACd;AAiDD,eAAO,MAAM,WAAW,GACtB,WAAW,WAAW,EACtB,MAAM,KAAK,EACX,UAAU,QAAQ,EAClB,UAAU,YAAY,GAAG,IAAI,KAC5B,YAAY,GAAG,IAiBjB,CAAC"}
@@ -1,10 +1,42 @@
1
+ function parsePixels(value) {
2
+ const parsed = Number.parseFloat(value);
3
+ return Number.isFinite(parsed) ? parsed : 0;
4
+ }
5
+ function fallbackDimensions(container) {
6
+ if (typeof container.querySelector !== "function")
7
+ return null;
8
+ const terminal = container.querySelector(".xterm");
9
+ const measure = container.querySelector(".xterm-helper-textarea");
10
+ if (!terminal || !measure)
11
+ return null;
12
+ const terminalStyle = window.getComputedStyle(terminal);
13
+ const cell = measure.getBoundingClientRect();
14
+ if (cell.width <= 0 || cell.height <= 0)
15
+ return null;
16
+ const paddingX = parsePixels(terminalStyle.paddingLeft) +
17
+ parsePixels(terminalStyle.paddingRight);
18
+ const paddingY = parsePixels(terminalStyle.paddingTop) +
19
+ parsePixels(terminalStyle.paddingBottom);
20
+ const scrollbarWidth = 14;
21
+ const availableWidth = Math.max(0, container.clientWidth - paddingX - scrollbarWidth);
22
+ const availableHeight = Math.max(0, container.clientHeight - paddingY);
23
+ return {
24
+ cols: Math.max(2, Math.floor(availableWidth / cell.width)),
25
+ rows: Math.max(1, Math.floor(availableHeight / cell.height)),
26
+ };
27
+ }
1
28
  export const fitTerminal = (container, term, fitAddon, lastSize) => {
2
29
  if (container.clientWidth === 0 || container.clientHeight === 0)
3
30
  return lastSize;
4
- fitAddon.fit();
5
- const nextSize = { cols: term.cols, rows: term.rows };
31
+ const fallback = fallbackDimensions(container);
32
+ const proposed = fitAddon.proposeDimensions();
33
+ const nextSize = fallback ?? proposed;
34
+ if (!nextSize)
35
+ return lastSize;
36
+ if (term.cols !== nextSize.cols || term.rows !== nextSize.rows) {
37
+ term.resize(nextSize.cols, nextSize.rows);
38
+ }
6
39
  if (lastSize?.cols === nextSize.cols && lastSize.rows === nextSize.rows)
7
40
  return lastSize;
8
41
  return nextSize;
9
42
  };
10
- //# sourceMappingURL=fitTerminal.js.map
package/dist/index.d.ts CHANGED
@@ -1,10 +1,21 @@
1
- export { TermkitProvider } from "./context.js";
2
- export type { TermkitProviderProps } from "./context.js";
1
+ export { TermkitProvider } from "./TermkitProvider.js";
2
+ export type { TermkitProviderProps } from "./TermkitProvider.js";
3
3
  export { Terminal } from "./Terminal.js";
4
4
  export type { TerminalProps } from "./Terminal.js";
5
- export { useSessions, useSession, useRingingSessions, useIsRinging, useSessionStatus, useFocusedSession, useIsFocused, useSlots, useDanglingSessions, useTermkitActions, } from "./hooks.js";
6
- export type { TermkitActions } from "./hooks.js";
7
- export type { TerminalSlot } from "./store.js";
5
+ export { useSessions } from "./useSessions.js";
6
+ export { useSession } from "./useSession.js";
7
+ export { useRingingSessions } from "./useRingingSessions.js";
8
+ export { useIsRinging } from "./useIsRinging.js";
9
+ export { useSessionStatus } from "./useSessionStatus.js";
10
+ export { useFocusedSession } from "./useFocusedSession.js";
11
+ export { useIsFocused } from "./useIsFocused.js";
12
+ export { useSlots } from "./useSlots.js";
13
+ export { useDanglingSessions } from "./useDanglingSessions.js";
14
+ export { useTermkitActions } from "./useTermkitActions.js";
15
+ export type { TermkitActions } from "./useTermkitActions.js";
16
+ export type { TerminalSlot } from "./createTermkitStore.js";
8
17
  export type { TermkitConfig, TermkitTransport, TermkitTerminalOptions, SessionStatus, LiveSession, } from "./types.js";
18
+ export { terminalColorSchemes } from "./terminalColors.js";
19
+ export type { TerminalColors, TerminalColorSchemeName, } from "./terminalColors.js";
9
20
  export { generateSessionName, SESSION_NAMES } from "@mp-lb/termkit-core";
10
21
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,eAAe,EAAE,MAAM,cAAc,CAAA;AAC9C,YAAY,EAAE,oBAAoB,EAAE,MAAM,cAAc,CAAA;AACxD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAA;AACxC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAA;AAElD,OAAO,EACL,WAAW,EACX,UAAU,EACV,kBAAkB,EAClB,YAAY,EACZ,gBAAgB,EAChB,iBAAiB,EACjB,YAAY,EACZ,QAAQ,EACR,mBAAmB,EACnB,iBAAiB,GAClB,MAAM,YAAY,CAAA;AACnB,YAAY,EAAE,cAAc,EAAE,MAAM,YAAY,CAAA;AAChD,YAAY,EAAE,YAAY,EAAE,MAAM,YAAY,CAAA;AAE9C,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAA;AAGnB,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAA"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAQA,OAAO,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AACvD,YAAY,EAAE,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACjE,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,YAAY,EAAE,aAAa,EAAE,MAAM,eAAe,CAAC;AAEnD,OAAO,EAAE,WAAW,EAAE,MAAM,kBAAkB,CAAC;AAC/C,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,OAAO,EAAE,kBAAkB,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,gBAAgB,EAAE,MAAM,uBAAuB,CAAC;AACzD,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,OAAO,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACjD,OAAO,EAAE,QAAQ,EAAE,MAAM,eAAe,CAAC;AACzC,OAAO,EAAE,mBAAmB,EAAE,MAAM,0BAA0B,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,wBAAwB,CAAC;AAC3D,YAAY,EAAE,cAAc,EAAE,MAAM,wBAAwB,CAAC;AAC7D,YAAY,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AAE5D,YAAY,EACV,aAAa,EACb,gBAAgB,EAChB,sBAAsB,EACtB,aAAa,EACb,WAAW,GACZ,MAAM,YAAY,CAAC;AACpB,OAAO,EAAE,oBAAoB,EAAE,MAAM,qBAAqB,CAAC;AAC3D,YAAY,EACV,cAAc,EACd,uBAAuB,GACxB,MAAM,qBAAqB,CAAC;AAG7B,OAAO,EAAE,mBAAmB,EAAE,aAAa,EAAE,MAAM,qBAAqB,CAAC"}