@yieldfabric/terminal 1.1.0 → 1.1.1

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/dist/index.cjs CHANGED
@@ -201,7 +201,7 @@ var handleSSEStream = async (url, body, onChunk, onComplete, onError, options) =
201
201
  ...method !== "GET" && method !== "HEAD" ? { "Content-Type": "application/json" } : {},
202
202
  ...options?.headers
203
203
  };
204
- const response = await fetch(url, {
204
+ const response = await (options?.fetcher ?? fetch)(url, {
205
205
  method,
206
206
  headers: requestHeaders,
207
207
  body: method === "GET" || method === "HEAD" ? void 0 : JSON.stringify(body),
@@ -3296,7 +3296,7 @@ function KGScopeDropdown({ items, onSelect, compact = false }) {
3296
3296
  }
3297
3297
  function ReasoningModeSelector({ current, onChange, compact = false }) {
3298
3298
  const t = useTheme();
3299
- const isLight = t.bgRaised === "#ffffff";
3299
+ const isLight = t.mode ? t.mode === "light" : t.bgRaised === "#ffffff";
3300
3300
  const isReason = current === "reason";
3301
3301
  const label = isReason ? "Reason" : "Auto";
3302
3302
  const title = isReason ? "Force deep reasoning \u2014 click to let the AI decide" : "AI decides \u2014 click to force deep reasoning";
@@ -3401,7 +3401,8 @@ function TerminalInputBar({
3401
3401
  }, []);
3402
3402
  const [focused, setFocused] = React15__default.default.useState(false);
3403
3403
  const chipsVisible = focused || currentCommand.trim().length > 0;
3404
- const isLight = t.bgRaised === "#ffffff";
3404
+ const isTouch = typeof window !== "undefined" && "ontouchstart" in window;
3405
+ const isLight = t.mode ? t.mode === "light" : t.bgRaised === "#ffffff";
3405
3406
  const hasCommand = Boolean(currentCommand.trim());
3406
3407
  const placeholderColor = isLight ? t.textMuted : "rgba(255,255,255,0.52)";
3407
3408
  const panelBorder = isLight ? t.border : "rgba(255,255,255,0.10)";
@@ -3441,7 +3442,7 @@ function TerminalInputBar({
3441
3442
  fitToContent(input);
3442
3443
  }, [currentCommand, inputRef]);
3443
3444
  return /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
3444
- /* @__PURE__ */ jsxRuntime.jsx("style", { children: `.yf-input-bar::placeholder { color: ${placeholderColor}; }` }),
3445
+ /* @__PURE__ */ jsxRuntime.jsx("style", { children: `.yf-input-bar::placeholder { color: ${placeholderColor}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }` }),
3445
3446
  commandPaletteItems.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs(
3446
3447
  "div",
3447
3448
  {
@@ -3487,10 +3488,17 @@ function TerminalInputBar({
3487
3488
  className: `rounded-xl border px-3 transition-[box-shadow,border-color] sm:px-4 ${focused ? "yf-input-focused" : ""}`,
3488
3489
  style: {
3489
3490
  backgroundColor: panelBg,
3490
- backdropFilter: "blur(18px)",
3491
+ // No backdrop blur on a touch screen. It is the one compositing
3492
+ // layer between the textarea and the page, and on iPhone the
3493
+ // caret was drawn a line below and a character right of where
3494
+ // typing lands — the caret rect resolved against the wrong
3495
+ // layer — while WebKit's own layout (checked in WebKit 26) puts
3496
+ // it on the line. The box keeps its colour; only the frosted
3497
+ // fabric behind it goes, which at this size is barely a change.
3498
+ backdropFilter: isTouch ? void 0 : "blur(18px)",
3491
3499
  borderColor: focused ? t.primary : panelBorder,
3492
3500
  boxShadow: focused ? `0 0 0 3px ${t.primaryMuted || "rgba(5,78,146,0.18)"}` : isLight ? "0 18px 70px -38px rgba(30,30,58,0.45)" : "0 18px 70px -38px rgba(0,0,0,0.95)",
3493
- WebkitBackdropFilter: "blur(18px)"
3501
+ WebkitBackdropFilter: isTouch ? void 0 : "blur(18px)"
3494
3502
  },
3495
3503
  children: [
3496
3504
  /* @__PURE__ */ jsxRuntime.jsx(
@@ -3594,7 +3602,22 @@ function TerminalInputBar({
3594
3602
  placeholder: isExecuting ? "Processing..." : placeholder,
3595
3603
  rows: 1,
3596
3604
  className: "yf-input-bar flex-1 min-w-[100px] bg-transparent focus:outline-none text-[16px] leading-6 resize-none overflow-hidden sm:text-sm sm:leading-5",
3597
- onFocus: () => setFocused(true),
3605
+ onFocus: () => {
3606
+ setFocused(true);
3607
+ if (!("ontouchstart" in window)) return;
3608
+ window.setTimeout(() => {
3609
+ const ta = inputRef.current;
3610
+ if (!ta || document.activeElement !== ta) return;
3611
+ let scroller = ta.parentElement;
3612
+ while (scroller && !/(auto|scroll)/.test(getComputedStyle(scroller).overflowY)) scroller = scroller.parentElement;
3613
+ window.scrollBy(0, 1);
3614
+ if (scroller) scroller.scrollTop += 1;
3615
+ window.requestAnimationFrame(() => {
3616
+ window.scrollBy(0, -1);
3617
+ if (scroller) scroller.scrollTop -= 1;
3618
+ });
3619
+ }, 400);
3620
+ },
3598
3621
  onBlur: () => setFocused(false),
3599
3622
  style: { color: t.text, caretColor: t.cursor },
3600
3623
  spellCheck: false,
@@ -3610,12 +3633,19 @@ function TerminalInputBar({
3610
3633
  disabled: !hasCommand || isExecuting || disabled,
3611
3634
  className: "h-10 w-10 rounded-xl disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center transition-all duration-200 shrink-0 sm:h-8 sm:w-8",
3612
3635
  style: {
3613
- backgroundColor: hasCommand ? t.primary : isLight ? t.bgRaisedAlt : "rgba(255,255,255,0.08)",
3636
+ backgroundColor: isLight ? t.primary : hasCommand ? t.primary : "rgba(255,255,255,0.08)",
3614
3637
  // `textInverse` is the theme's "text on a filled button".
3615
3638
  // `bgBase` was used here before and is `transparent` in
3616
3639
  // every app theme — so the chevron vanished the moment
3617
3640
  // there was something to send.
3618
- color: hasCommand ? t.textInverse : isLight ? t.textMuted : "rgba(255,255,255,0.72)"
3641
+ // In LIGHT mode this is always a solid accent fill with white
3642
+ // on it (6.1:1). It is the app's primary verb and the single
3643
+ // most-pressed control; a pale disc — grey, or a 10% tint —
3644
+ // read as disabled even when it was not. The empty state is
3645
+ // already expressed by `disabled:opacity-60`, so the fill does
3646
+ // not also have to whisper. Dark keeps its white-alpha rest,
3647
+ // where a solid orange disc would shout on near-black.
3648
+ color: isLight ? t.textInverse : hasCommand ? t.textInverse : "rgba(255,255,255,0.72)"
3619
3649
  },
3620
3650
  onMouseEnter: (e) => {
3621
3651
  if (hasCommand) {
@@ -3626,7 +3656,7 @@ function TerminalInputBar({
3626
3656
  },
3627
3657
  onMouseLeave: (e) => {
3628
3658
  e.currentTarget.style.filter = "";
3629
- e.currentTarget.style.backgroundColor = hasCommand ? t.primary : isLight ? t.bgRaisedAlt : "rgba(255,255,255,0.08)";
3659
+ e.currentTarget.style.backgroundColor = isLight ? t.primary : hasCommand ? t.primary : "rgba(255,255,255,0.08)";
3630
3660
  },
3631
3661
  title: submitTitle,
3632
3662
  "aria-label": submitTitle,
@@ -3750,7 +3780,7 @@ var KIND_DOT = {
3750
3780
  };
3751
3781
  function SlashMentionPicker({ candidates, selectedIndex, onPick }) {
3752
3782
  const t = useTheme();
3753
- const isLight = t.bgRaised === "#ffffff";
3783
+ const isLight = t.mode ? t.mode === "light" : t.bgRaised === "#ffffff";
3754
3784
  if (candidates.length === 0) return null;
3755
3785
  return /* @__PURE__ */ jsxRuntime.jsxs(
3756
3786
  "div",
package/dist/index.d.cts CHANGED
@@ -21,6 +21,16 @@ import { NodeProps, EdgeProps, Node, Edge } from '@xyflow/react';
21
21
  type ReasoningMode = 'reason';
22
22
 
23
23
  interface TerminalTheme {
24
+ /** Which side of the light/dark divide this theme is.
25
+ *
26
+ * Components that need to branch (a placeholder tint, a panel border, a
27
+ * toolbar fill) used to sniff it with `t.bgRaised === '#ffffff'`. That is an
28
+ * exact-string test against one literal, so it silently reported DARK for
29
+ * any light theme whose surfaces are CSS variables, or simply an off-white
30
+ * — which is what happens the moment a host themes its surfaces properly.
31
+ * Declare it instead. Optional, and the old sniff remains the fallback, so
32
+ * a theme that predates this keeps its current behaviour. */
33
+ mode?: 'light' | 'dark';
24
34
  primary: string;
25
35
  primaryMuted: string;
26
36
  primaryHover: string;
@@ -1354,6 +1364,9 @@ declare const handleSSEStream: (url: string, body: any, onChunk: (chunk: any) =>
1354
1364
  method?: string;
1355
1365
  headers?: Record<string, string>;
1356
1366
  signal?: AbortSignal;
1367
+ /** Host authentication/renewal transport. It must preserve the supplied
1368
+ * abort signal and must not retry a request after its stream has begun. */
1369
+ fetcher?: (url: string, init: RequestInit) => Promise<Response>;
1357
1370
  onGone?: (reason: string) => void;
1358
1371
  /** Require an explicit answer terminal event. Defaults to true.
1359
1372
  * Set false only for non-answer protocols that intentionally end at EOF.
package/dist/index.d.ts CHANGED
@@ -21,6 +21,16 @@ import { NodeProps, EdgeProps, Node, Edge } from '@xyflow/react';
21
21
  type ReasoningMode = 'reason';
22
22
 
23
23
  interface TerminalTheme {
24
+ /** Which side of the light/dark divide this theme is.
25
+ *
26
+ * Components that need to branch (a placeholder tint, a panel border, a
27
+ * toolbar fill) used to sniff it with `t.bgRaised === '#ffffff'`. That is an
28
+ * exact-string test against one literal, so it silently reported DARK for
29
+ * any light theme whose surfaces are CSS variables, or simply an off-white
30
+ * — which is what happens the moment a host themes its surfaces properly.
31
+ * Declare it instead. Optional, and the old sniff remains the fallback, so
32
+ * a theme that predates this keeps its current behaviour. */
33
+ mode?: 'light' | 'dark';
24
34
  primary: string;
25
35
  primaryMuted: string;
26
36
  primaryHover: string;
@@ -1354,6 +1364,9 @@ declare const handleSSEStream: (url: string, body: any, onChunk: (chunk: any) =>
1354
1364
  method?: string;
1355
1365
  headers?: Record<string, string>;
1356
1366
  signal?: AbortSignal;
1367
+ /** Host authentication/renewal transport. It must preserve the supplied
1368
+ * abort signal and must not retry a request after its stream has begun. */
1369
+ fetcher?: (url: string, init: RequestInit) => Promise<Response>;
1357
1370
  onGone?: (reason: string) => void;
1358
1371
  /** Require an explicit answer terminal event. Defaults to true.
1359
1372
  * Set false only for non-answer protocols that intentionally end at EOF.
package/dist/index.js CHANGED
@@ -192,7 +192,7 @@ var handleSSEStream = async (url, body, onChunk, onComplete, onError, options) =
192
192
  ...method !== "GET" && method !== "HEAD" ? { "Content-Type": "application/json" } : {},
193
193
  ...options?.headers
194
194
  };
195
- const response = await fetch(url, {
195
+ const response = await (options?.fetcher ?? fetch)(url, {
196
196
  method,
197
197
  headers: requestHeaders,
198
198
  body: method === "GET" || method === "HEAD" ? void 0 : JSON.stringify(body),
@@ -3287,7 +3287,7 @@ function KGScopeDropdown({ items, onSelect, compact = false }) {
3287
3287
  }
3288
3288
  function ReasoningModeSelector({ current, onChange, compact = false }) {
3289
3289
  const t = useTheme();
3290
- const isLight = t.bgRaised === "#ffffff";
3290
+ const isLight = t.mode ? t.mode === "light" : t.bgRaised === "#ffffff";
3291
3291
  const isReason = current === "reason";
3292
3292
  const label = isReason ? "Reason" : "Auto";
3293
3293
  const title = isReason ? "Force deep reasoning \u2014 click to let the AI decide" : "AI decides \u2014 click to force deep reasoning";
@@ -3392,7 +3392,8 @@ function TerminalInputBar({
3392
3392
  }, []);
3393
3393
  const [focused, setFocused] = React15.useState(false);
3394
3394
  const chipsVisible = focused || currentCommand.trim().length > 0;
3395
- const isLight = t.bgRaised === "#ffffff";
3395
+ const isTouch = typeof window !== "undefined" && "ontouchstart" in window;
3396
+ const isLight = t.mode ? t.mode === "light" : t.bgRaised === "#ffffff";
3396
3397
  const hasCommand = Boolean(currentCommand.trim());
3397
3398
  const placeholderColor = isLight ? t.textMuted : "rgba(255,255,255,0.52)";
3398
3399
  const panelBorder = isLight ? t.border : "rgba(255,255,255,0.10)";
@@ -3432,7 +3433,7 @@ function TerminalInputBar({
3432
3433
  fitToContent(input);
3433
3434
  }, [currentCommand, inputRef]);
3434
3435
  return /* @__PURE__ */ jsxs(Fragment, { children: [
3435
- /* @__PURE__ */ jsx("style", { children: `.yf-input-bar::placeholder { color: ${placeholderColor}; }` }),
3436
+ /* @__PURE__ */ jsx("style", { children: `.yf-input-bar::placeholder { color: ${placeholderColor}; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }` }),
3436
3437
  commandPaletteItems.length > 0 && /* @__PURE__ */ jsxs(
3437
3438
  "div",
3438
3439
  {
@@ -3478,10 +3479,17 @@ function TerminalInputBar({
3478
3479
  className: `rounded-xl border px-3 transition-[box-shadow,border-color] sm:px-4 ${focused ? "yf-input-focused" : ""}`,
3479
3480
  style: {
3480
3481
  backgroundColor: panelBg,
3481
- backdropFilter: "blur(18px)",
3482
+ // No backdrop blur on a touch screen. It is the one compositing
3483
+ // layer between the textarea and the page, and on iPhone the
3484
+ // caret was drawn a line below and a character right of where
3485
+ // typing lands — the caret rect resolved against the wrong
3486
+ // layer — while WebKit's own layout (checked in WebKit 26) puts
3487
+ // it on the line. The box keeps its colour; only the frosted
3488
+ // fabric behind it goes, which at this size is barely a change.
3489
+ backdropFilter: isTouch ? void 0 : "blur(18px)",
3482
3490
  borderColor: focused ? t.primary : panelBorder,
3483
3491
  boxShadow: focused ? `0 0 0 3px ${t.primaryMuted || "rgba(5,78,146,0.18)"}` : isLight ? "0 18px 70px -38px rgba(30,30,58,0.45)" : "0 18px 70px -38px rgba(0,0,0,0.95)",
3484
- WebkitBackdropFilter: "blur(18px)"
3492
+ WebkitBackdropFilter: isTouch ? void 0 : "blur(18px)"
3485
3493
  },
3486
3494
  children: [
3487
3495
  /* @__PURE__ */ jsx(
@@ -3585,7 +3593,22 @@ function TerminalInputBar({
3585
3593
  placeholder: isExecuting ? "Processing..." : placeholder,
3586
3594
  rows: 1,
3587
3595
  className: "yf-input-bar flex-1 min-w-[100px] bg-transparent focus:outline-none text-[16px] leading-6 resize-none overflow-hidden sm:text-sm sm:leading-5",
3588
- onFocus: () => setFocused(true),
3596
+ onFocus: () => {
3597
+ setFocused(true);
3598
+ if (!("ontouchstart" in window)) return;
3599
+ window.setTimeout(() => {
3600
+ const ta = inputRef.current;
3601
+ if (!ta || document.activeElement !== ta) return;
3602
+ let scroller = ta.parentElement;
3603
+ while (scroller && !/(auto|scroll)/.test(getComputedStyle(scroller).overflowY)) scroller = scroller.parentElement;
3604
+ window.scrollBy(0, 1);
3605
+ if (scroller) scroller.scrollTop += 1;
3606
+ window.requestAnimationFrame(() => {
3607
+ window.scrollBy(0, -1);
3608
+ if (scroller) scroller.scrollTop -= 1;
3609
+ });
3610
+ }, 400);
3611
+ },
3589
3612
  onBlur: () => setFocused(false),
3590
3613
  style: { color: t.text, caretColor: t.cursor },
3591
3614
  spellCheck: false,
@@ -3601,12 +3624,19 @@ function TerminalInputBar({
3601
3624
  disabled: !hasCommand || isExecuting || disabled,
3602
3625
  className: "h-10 w-10 rounded-xl disabled:opacity-60 disabled:cursor-not-allowed flex items-center justify-center transition-all duration-200 shrink-0 sm:h-8 sm:w-8",
3603
3626
  style: {
3604
- backgroundColor: hasCommand ? t.primary : isLight ? t.bgRaisedAlt : "rgba(255,255,255,0.08)",
3627
+ backgroundColor: isLight ? t.primary : hasCommand ? t.primary : "rgba(255,255,255,0.08)",
3605
3628
  // `textInverse` is the theme's "text on a filled button".
3606
3629
  // `bgBase` was used here before and is `transparent` in
3607
3630
  // every app theme — so the chevron vanished the moment
3608
3631
  // there was something to send.
3609
- color: hasCommand ? t.textInverse : isLight ? t.textMuted : "rgba(255,255,255,0.72)"
3632
+ // In LIGHT mode this is always a solid accent fill with white
3633
+ // on it (6.1:1). It is the app's primary verb and the single
3634
+ // most-pressed control; a pale disc — grey, or a 10% tint —
3635
+ // read as disabled even when it was not. The empty state is
3636
+ // already expressed by `disabled:opacity-60`, so the fill does
3637
+ // not also have to whisper. Dark keeps its white-alpha rest,
3638
+ // where a solid orange disc would shout on near-black.
3639
+ color: isLight ? t.textInverse : hasCommand ? t.textInverse : "rgba(255,255,255,0.72)"
3610
3640
  },
3611
3641
  onMouseEnter: (e) => {
3612
3642
  if (hasCommand) {
@@ -3617,7 +3647,7 @@ function TerminalInputBar({
3617
3647
  },
3618
3648
  onMouseLeave: (e) => {
3619
3649
  e.currentTarget.style.filter = "";
3620
- e.currentTarget.style.backgroundColor = hasCommand ? t.primary : isLight ? t.bgRaisedAlt : "rgba(255,255,255,0.08)";
3650
+ e.currentTarget.style.backgroundColor = isLight ? t.primary : hasCommand ? t.primary : "rgba(255,255,255,0.08)";
3621
3651
  },
3622
3652
  title: submitTitle,
3623
3653
  "aria-label": submitTitle,
@@ -3741,7 +3771,7 @@ var KIND_DOT = {
3741
3771
  };
3742
3772
  function SlashMentionPicker({ candidates, selectedIndex, onPick }) {
3743
3773
  const t = useTheme();
3744
- const isLight = t.bgRaised === "#ffffff";
3774
+ const isLight = t.mode ? t.mode === "light" : t.bgRaised === "#ffffff";
3745
3775
  if (candidates.length === 0) return null;
3746
3776
  return /* @__PURE__ */ jsxs(
3747
3777
  "div",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@yieldfabric/terminal",
3
- "version": "1.1.0",
3
+ "version": "1.1.1",
4
4
  "description": "Reusable AI-powered terminal + knowledge-graph component for YieldFabric",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "type": "module",