@burdenoff/website-sdk 2026.921.1 → 2026.922.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.js CHANGED
@@ -8029,6 +8029,7 @@ function toPayload(staged) {
8029
8029
  textContent
8030
8030
  }));
8031
8031
  }
8032
+ var SHORT_VIEWPORT_PX = 420;
8032
8033
  var EXPLORE_CSS = `
8033
8034
  @keyframes boff-explore-pulse {
8034
8035
  0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
@@ -8063,6 +8064,12 @@ var EXPLORE_CSS = `
8063
8064
  .boff-explore-scroll {
8064
8065
  scrollbar-width: thin;
8065
8066
  scrollbar-color: hsl(var(--border)) transparent;
8067
+ /* Stop the transcript handing its scroll to the page. Without this, the first
8068
+ swipe that exhausts the transcript chains to the document \u2014 and on a host site
8069
+ that is 65-70% footer, which is how a phone ends up showing nothing but the
8070
+ footer. Engines that do not support this property ignore the line and keep
8071
+ today's behaviour; the scroll correction above covers them. */
8072
+ overscroll-behavior: contain;
8066
8073
  }
8067
8074
  .boff-explore-scroll::-webkit-scrollbar { width: 10px; }
8068
8075
  .boff-explore-scroll::-webkit-scrollbar-track { background: transparent; }
@@ -8180,6 +8187,68 @@ var EXPLORE_CSS = `
8180
8187
  .boff-explore-busy::after { animation: none; }
8181
8188
  .boff-explore-rise { animation: none; opacity: 1; transform: none; }
8182
8189
  }
8190
+
8191
+ /* ---- Pane height ----------------------------------------------------------------------
8192
+ This used to be one inline declaration, \`height: calc(100dvh - 64px)\`, and it carried two
8193
+ defects. An inline style cannot carry a fallback, so every engine without \`dvh\` (iOS
8194
+ Safari < 15.4, Chrome/WebView < 108, Samsung Internet < 21, Firefox < 101) dropped it
8195
+ whole; and the 64px came from a prop hard-coded to 64 on all 41 host pages that matches
8196
+ none of them \u2014 the burdenoff/algoshred chrome is 80px, the sticky-header sites are 65.
8197
+
8198
+ A class plus custom properties lets the cascade do the work: \`vh\` is the floor and \`dvh\`
8199
+ an upgrade where it exists, while the two measured values stay *lengths*, never a height.
8200
+ That distinction is the whole prerender contract. The host sites snapshot \`/explore\` by
8201
+ loading the built SPA in headless Chromium and writing the POST-HYDRATION DOM to
8202
+ dist/explore/index.html, so every attribute and inline value this component sets after
8203
+ mount IS the static HTML CloudFront serves. A measured pixel *height* would therefore
8204
+ hand every phone the prerender viewport's height until the bundle executes; a measured
8205
+ *offset* is a site-header height, which is the same on every device to within a few
8206
+ pixels, and the height built from it stays viewport-relative on the visitor's own screen.
8207
+
8208
+ --boff-explore-offset chrome above the pane (measured root top; the prop pre-measure)
8209
+ --boff-explore-inset the slice of the layout viewport hidden below the visible area \u2014
8210
+ the on-screen keyboard, which \`dvh\` does not track. Zero unless
8211
+ something is actually covering the bottom, so a snapshot taken
8212
+ with no keyboard open never carries it. */
8213
+ .boff-explore-viewport {
8214
+ height: calc(100vh - var(--boff-explore-offset, 0px) - var(--boff-explore-inset, 0px));
8215
+ }
8216
+ @supports (height: 100dvh) {
8217
+ .boff-explore-viewport {
8218
+ height: calc(100dvh - var(--boff-explore-offset, 0px) - var(--boff-explore-inset, 0px));
8219
+ }
8220
+ }
8221
+
8222
+ /* Landscape phones and other short viewports: a one-screen pane leaves a ~124px transcript
8223
+ sliver under ~610px of chrome and content. At or below the threshold the pane stops
8224
+ pretending to be a window and lets the page's own scrolling carry the transcript. The
8225
+ min-height is the available height rather than a fixed rem value, so the fallback can
8226
+ never be taller than the screen it is rescuing. */
8227
+ @media (max-height: ${SHORT_VIEWPORT_PX}px) {
8228
+ .boff-explore-viewport {
8229
+ height: auto;
8230
+ min-height: calc(100vh - var(--boff-explore-offset, 0px));
8231
+ }
8232
+ }
8233
+ @supports (height: 100dvh) {
8234
+ @media (max-height: ${SHORT_VIEWPORT_PX}px) {
8235
+ .boff-explore-viewport {
8236
+ min-height: calc(100dvh - var(--boff-explore-offset, 0px));
8237
+ }
8238
+ }
8239
+ }
8240
+
8241
+ /* ---- Composer dock --------------------------------------------------------------------
8242
+ Three declarations, each the fallback for the one after it: a plain 1rem where \`max()\`
8243
+ is unknown, then the measured overlap of any FOREIGN fixed chrome sitting over the
8244
+ composer (the host sites' cookie banner is 229px tall and lands exactly there), then the
8245
+ same plus the home-indicator inset. \`--boff-explore-chrome\` is set on the root only once
8246
+ an overlap has actually been measured, so everywhere else this collapses to 1rem. */
8247
+ .boff-explore-composer {
8248
+ padding-bottom: 1rem;
8249
+ padding-bottom: max(1rem, var(--boff-explore-chrome, 0px));
8250
+ padding-bottom: max(1rem, env(safe-area-inset-bottom), var(--boff-explore-chrome, 0px));
8251
+ }
8183
8252
  `;
8184
8253
  function ExploreStyles() {
8185
8254
  return /* @__PURE__ */ jsxRuntime.jsx("style", { dangerouslySetInnerHTML: { __html: EXPLORE_CSS } });
@@ -8201,6 +8270,8 @@ var CAPABILITIES = [
8201
8270
  body: "Get the sources and next steps \u2014 pricing, docs, a demo \u2014 without hunting."
8202
8271
  }
8203
8272
  ];
8273
+ var MIN_VISIBLE_FRACTION = 0.25;
8274
+ var SDK_OVERLAY_SELECTOR = "[data-boff-explore]";
8204
8275
  var CTA_VARIANTS = {
8205
8276
  CONTACT: "default",
8206
8277
  WAITLIST: "default",
@@ -8216,6 +8287,28 @@ function normalizeOffset(offset) {
8216
8287
  if (offset === void 0) return "0px";
8217
8288
  return typeof offset === "number" ? `${offset}px` : offset;
8218
8289
  }
8290
+ function isOwnElement(el, root) {
8291
+ if (el === document.body || el === document.documentElement) return true;
8292
+ if (root.contains(el)) return true;
8293
+ return el.closest(SDK_OVERLAY_SELECTOR) !== null;
8294
+ }
8295
+ function visualViewportHeight() {
8296
+ return window.visualViewport?.height ?? window.innerHeight;
8297
+ }
8298
+ function clamp(value, min, max) {
8299
+ return Math.min(Math.max(value, min), max);
8300
+ }
8301
+ function isRestoredNavigation() {
8302
+ const entries = window.performance?.getEntriesByType?.("navigation");
8303
+ const entry = entries?.[0];
8304
+ if (!entry) return true;
8305
+ const navigation = entry;
8306
+ return navigation.type === "reload" || navigation.type === "back_forward";
8307
+ }
8308
+ function pageOwnsScrolling(el) {
8309
+ if (window.innerHeight > SHORT_VIEWPORT_PX) return false;
8310
+ return el.scrollHeight - el.clientHeight <= 1;
8311
+ }
8219
8312
  function isInternalPath(url) {
8220
8313
  return url.startsWith("/") && !url.startsWith("//");
8221
8314
  }
@@ -9064,6 +9157,8 @@ function ExplorePage({
9064
9157
  const pickQueueRef = React.useRef(Promise.resolve());
9065
9158
  const [shareLabel, setShareLabel] = React.useState(T.share);
9066
9159
  const [shareUrl, setShareUrl] = React.useState(null);
9160
+ const rootRef = React.useRef(null);
9161
+ const composerRef = React.useRef(null);
9067
9162
  const scrollRef = React.useRef(null);
9068
9163
  const latestAssistantRef = React.useRef(null);
9069
9164
  const alignedForRef = React.useRef(null);
@@ -9091,11 +9186,196 @@ function ExplorePage({
9091
9186
  el.style.height = "auto";
9092
9187
  el.style.height = `${Math.min(el.scrollHeight, 200)}px`;
9093
9188
  }, [draft]);
9189
+ const [paneOffset, setPaneOffset] = React.useState(null);
9190
+ const [bottomInset, setBottomInset] = React.useState(0);
9191
+ const paneOffsetRef = React.useRef(0);
9192
+ const measurePane = React.useCallback(() => {
9193
+ if (heightMode !== "viewport") return;
9194
+ const el = rootRef.current;
9195
+ if (!el) return;
9196
+ const layoutHeight = window.innerHeight;
9197
+ if (layoutHeight === 0) return;
9198
+ const top = Math.round(
9199
+ clamp(el.getBoundingClientRect().top, 0, layoutHeight)
9200
+ );
9201
+ const viewport = window.visualViewport;
9202
+ const inset = viewport ? Math.round(
9203
+ clamp(
9204
+ layoutHeight - viewport.height - viewport.offsetTop,
9205
+ 0,
9206
+ layoutHeight
9207
+ )
9208
+ ) : 0;
9209
+ paneOffsetRef.current = top;
9210
+ setPaneOffset((current) => current === top ? current : top);
9211
+ setBottomInset((current) => current === inset ? current : inset);
9212
+ }, [heightMode]);
9213
+ const [chromeOverlap, setChromeOverlap] = React.useState(0);
9214
+ const measureChrome = React.useCallback(() => {
9215
+ const root = rootRef.current;
9216
+ const dock = composerRef.current;
9217
+ if (!root || !dock) return;
9218
+ if (typeof document.elementFromPoint !== "function") return;
9219
+ const viewportHeight = visualViewportHeight();
9220
+ const rect = dock.getBoundingClientRect();
9221
+ if (rect.width === 0 || viewportHeight === 0) return;
9222
+ if (rect.bottom <= 0 || rect.top >= viewportHeight) return;
9223
+ const edge = Math.min(rect.bottom, viewportHeight);
9224
+ const hit = document.elementFromPoint(rect.left + rect.width / 2, edge - 1);
9225
+ if (!hit || isOwnElement(hit, root)) {
9226
+ setChromeOverlap(0);
9227
+ return;
9228
+ }
9229
+ const overlap = Math.round(edge - hit.getBoundingClientRect().top);
9230
+ setChromeOverlap(overlap > 0 && overlap < viewportHeight ? overlap : 0);
9231
+ }, []);
9232
+ const remeasure = React.useCallback(() => {
9233
+ measurePane();
9234
+ measureChrome();
9235
+ }, [measurePane, measureChrome]);
9236
+ React.useEffect(() => {
9237
+ if (typeof window === "undefined") return;
9238
+ let frame = 0;
9239
+ const run = () => {
9240
+ frame = 0;
9241
+ remeasure();
9242
+ };
9243
+ const schedule = () => {
9244
+ if (frame) return;
9245
+ if (typeof window.requestAnimationFrame !== "function") {
9246
+ remeasure();
9247
+ return;
9248
+ }
9249
+ frame = window.requestAnimationFrame(run);
9250
+ };
9251
+ remeasure();
9252
+ const viewport = window.visualViewport;
9253
+ window.addEventListener("resize", schedule);
9254
+ window.addEventListener("orientationchange", schedule);
9255
+ viewport?.addEventListener("resize", schedule);
9256
+ viewport?.addEventListener("scroll", schedule);
9257
+ const observer = typeof MutationObserver === "function" ? new MutationObserver((records) => {
9258
+ const el = rootRef.current;
9259
+ if (el && records.every((record) => el.contains(record.target)))
9260
+ return;
9261
+ schedule();
9262
+ }) : null;
9263
+ observer?.observe(document.body, { childList: true, subtree: true });
9264
+ return () => {
9265
+ if (frame) window.cancelAnimationFrame(frame);
9266
+ window.removeEventListener("resize", schedule);
9267
+ window.removeEventListener("orientationchange", schedule);
9268
+ viewport?.removeEventListener("resize", schedule);
9269
+ viewport?.removeEventListener("scroll", schedule);
9270
+ observer?.disconnect();
9271
+ setPaneOffset(null);
9272
+ setBottomInset(0);
9273
+ setChromeOverlap(0);
9274
+ };
9275
+ }, [remeasure]);
9276
+ React.useEffect(() => {
9277
+ measureChrome();
9278
+ }, [measureChrome, paneOffset, bottomInset]);
9279
+ React.useEffect(() => {
9280
+ if (heightMode !== "viewport" || typeof window === "undefined") return;
9281
+ const { history: browserHistory } = window;
9282
+ const previousRestoration = "scrollRestoration" in browserHistory ? browserHistory.scrollRestoration : null;
9283
+ if (previousRestoration !== null)
9284
+ browserHistory.scrollRestoration = "manual";
9285
+ let frame = 0;
9286
+ let visitorMoved = false;
9287
+ const onVisitorMove = () => {
9288
+ visitorMoved = true;
9289
+ };
9290
+ const align = () => {
9291
+ if (visitorMoved) return;
9292
+ const el = rootRef.current;
9293
+ if (!el) return;
9294
+ const viewportHeight = visualViewportHeight();
9295
+ const rect = el.getBoundingClientRect();
9296
+ if (rect.height === 0 || viewportHeight === 0) return;
9297
+ const visible = Math.min(rect.bottom, viewportHeight) - Math.max(rect.top, 0);
9298
+ const reachable = Math.min(rect.height, viewportHeight);
9299
+ if (visible >= reachable * MIN_VISIBLE_FRACTION) return;
9300
+ window.scrollTo({
9301
+ top: Math.max(0, window.scrollY + rect.top),
9302
+ left: window.scrollX,
9303
+ behavior: "auto"
9304
+ });
9305
+ if (typeof document.elementFromPoint === "function") {
9306
+ const probe = document.elementFromPoint(
9307
+ Math.max(0, rect.left + rect.width / 2),
9308
+ 1
9309
+ );
9310
+ if (probe && !isOwnElement(probe, el)) {
9311
+ const chrome = probe.getBoundingClientRect().bottom;
9312
+ if (chrome > 0 && chrome < viewportHeight / 2) {
9313
+ window.scrollBy({ top: -chrome, left: 0, behavior: "auto" });
9314
+ }
9315
+ }
9316
+ }
9317
+ remeasure();
9318
+ };
9319
+ const alignIfRestored = () => {
9320
+ if (!isRestoredNavigation()) return;
9321
+ align();
9322
+ };
9323
+ alignIfRestored();
9324
+ if (typeof window.requestAnimationFrame === "function") {
9325
+ frame = window.requestAnimationFrame(alignIfRestored);
9326
+ }
9327
+ const onPageShow = (event) => {
9328
+ if (!event.persisted) {
9329
+ alignIfRestored();
9330
+ return;
9331
+ }
9332
+ visitorMoved = false;
9333
+ align();
9334
+ };
9335
+ window.addEventListener("pageshow", onPageShow);
9336
+ window.addEventListener("wheel", onVisitorMove, { passive: true });
9337
+ window.addEventListener("touchmove", onVisitorMove, { passive: true });
9338
+ window.addEventListener("keydown", onVisitorMove, { passive: true });
9339
+ return () => {
9340
+ if (frame) window.cancelAnimationFrame(frame);
9341
+ window.removeEventListener("pageshow", onPageShow);
9342
+ window.removeEventListener("wheel", onVisitorMove);
9343
+ window.removeEventListener("touchmove", onVisitorMove);
9344
+ window.removeEventListener("keydown", onVisitorMove);
9345
+ if (previousRestoration !== null)
9346
+ browserHistory.scrollRestoration = previousRestoration;
9347
+ };
9348
+ }, [heightMode, remeasure]);
9094
9349
  const handleScroll = React.useCallback(() => {
9095
9350
  const el = scrollRef.current;
9096
9351
  if (!el) return;
9097
9352
  stickToBottomRef.current = el.scrollHeight - el.scrollTop - el.clientHeight < 80;
9098
9353
  }, []);
9354
+ React.useEffect(() => {
9355
+ if (typeof window === "undefined") return;
9356
+ let frame = 0;
9357
+ const run = () => {
9358
+ frame = 0;
9359
+ const el = scrollRef.current;
9360
+ const dock = composerRef.current;
9361
+ if (!el || !dock) return;
9362
+ if (!pageOwnsScrolling(el)) return;
9363
+ stickToBottomRef.current = dock.getBoundingClientRect().bottom - visualViewportHeight() < 80;
9364
+ };
9365
+ const schedule = () => {
9366
+ if (frame) return;
9367
+ if (typeof window.requestAnimationFrame !== "function") {
9368
+ run();
9369
+ return;
9370
+ }
9371
+ frame = window.requestAnimationFrame(run);
9372
+ };
9373
+ window.addEventListener("scroll", schedule, { passive: true });
9374
+ return () => {
9375
+ if (frame) window.cancelAnimationFrame(frame);
9376
+ window.removeEventListener("scroll", schedule);
9377
+ };
9378
+ }, []);
9099
9379
  const latestAssistantId = React.useMemo(() => {
9100
9380
  for (let i = messages.length - 1; i >= 0; i -= 1) {
9101
9381
  const message = messages[i];
@@ -9111,16 +9391,32 @@ function ExplorePage({
9111
9391
  if (!stickToBottomRef.current) return;
9112
9392
  if (anchor.offsetHeight === 0) return;
9113
9393
  alignedForRef.current = latestAssistantId;
9114
- const delta = anchor.getBoundingClientRect().top - el.getBoundingClientRect().top;
9115
- el.scrollTop = Math.max(0, el.scrollTop + delta - 12);
9394
+ if (!pageOwnsScrolling(el)) {
9395
+ const delta2 = anchor.getBoundingClientRect().top - el.getBoundingClientRect().top;
9396
+ el.scrollTop = Math.max(0, el.scrollTop + delta2 - 12);
9397
+ return;
9398
+ }
9399
+ const delta = Math.round(
9400
+ anchor.getBoundingClientRect().top - paneOffsetRef.current - 12
9401
+ );
9402
+ if (delta !== 0) window.scrollBy({ top: delta, left: 0, behavior: "auto" });
9116
9403
  }, [latestAssistantId, messages]);
9117
9404
  React.useEffect(() => {
9405
+ if (messages.length === 0) return;
9118
9406
  if (!stickToBottomRef.current) return;
9119
9407
  if (latestAssistantId && alignedForRef.current === latestAssistantId)
9120
9408
  return;
9121
9409
  const el = scrollRef.current;
9122
9410
  if (!el) return;
9123
- el.scrollTop = el.scrollHeight;
9411
+ if (!pageOwnsScrolling(el)) {
9412
+ el.scrollTop = el.scrollHeight;
9413
+ return;
9414
+ }
9415
+ const dock = composerRef.current ?? el;
9416
+ const delta = Math.round(
9417
+ dock.getBoundingClientRect().bottom - visualViewportHeight()
9418
+ );
9419
+ if (delta > 0) window.scrollBy({ top: delta, left: 0, behavior: "auto" });
9124
9420
  }, [activity, latestAssistantId, messages]);
9125
9421
  const handleShare = React.useCallback(async () => {
9126
9422
  setShareLabel(T.copying);
@@ -9218,16 +9514,27 @@ function ExplorePage({
9218
9514
  const captchaOn = Boolean(
9219
9515
  catalog.captchaRequired && catalog.recaptchaSiteKey
9220
9516
  );
9221
- const rootStyle = heightMode === "viewport" ? { height: `calc(100dvh - ${normalizeOffset(viewportOffset)})` } : void 0;
9517
+ const rootStyle = {};
9518
+ if (heightMode === "viewport") {
9519
+ rootStyle["--boff-explore-offset"] = paneOffset === null ? normalizeOffset(viewportOffset) : `${paneOffset}px`;
9520
+ if (bottomInset > 0) {
9521
+ rootStyle["--boff-explore-inset"] = `${bottomInset}px`;
9522
+ }
9523
+ }
9524
+ if (chromeOverlap > 0) {
9525
+ rootStyle["--boff-explore-chrome"] = `${chromeOverlap}px`;
9526
+ }
9222
9527
  return /* @__PURE__ */ jsxRuntime.jsxs(
9223
9528
  "div",
9224
9529
  {
9530
+ ref: rootRef,
9225
9531
  "data-boff-explore": "page",
9226
9532
  "data-phase": phase,
9227
9533
  "data-locale": activeLocale,
9228
9534
  dir: rtl ? "rtl" : void 0,
9229
9535
  className: cn(
9230
9536
  "flex w-full min-w-0 flex-col overflow-x-hidden bg-background text-foreground",
9537
+ heightMode === "viewport" && "boff-explore-viewport",
9231
9538
  heightMode === "auto" && "min-h-[70vh]",
9232
9539
  className
9233
9540
  ),
@@ -9461,249 +9768,257 @@ function ExplorePage({
9461
9768
  ]
9462
9769
  }
9463
9770
  ) : null,
9464
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "border-t border-border bg-background/85 px-4 pb-[max(1rem,env(safe-area-inset-bottom))] pt-3 backdrop-blur-md", children: /* @__PURE__ */ jsxRuntime.jsxs(
9465
- "form",
9771
+ /* @__PURE__ */ jsxRuntime.jsx(
9772
+ "div",
9466
9773
  {
9467
- "data-boff-explore": "composer",
9468
- className: "mx-auto w-full max-w-3xl",
9469
- onSubmit: (event) => {
9470
- event.preventDefault();
9471
- submitDraft();
9472
- },
9473
- children: [
9474
- error ? /* @__PURE__ */ jsxRuntime.jsx(ErrorBanner, { error, onRetry: retry, onReset: reset }) : null,
9475
- staged.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
9476
- "ul",
9477
- {
9478
- "data-boff-explore": "attachments",
9479
- className: "mb-2 flex flex-wrap gap-1.5",
9480
- "aria-label": T.attach,
9481
- children: staged.map((entry) => /* @__PURE__ */ jsxRuntime.jsxs(
9482
- "li",
9774
+ ref: composerRef,
9775
+ "data-boff-explore": "composer-dock",
9776
+ className: "boff-explore-composer border-t border-border bg-background/85 px-4 pt-3 backdrop-blur-md",
9777
+ children: /* @__PURE__ */ jsxRuntime.jsxs(
9778
+ "form",
9779
+ {
9780
+ "data-boff-explore": "composer",
9781
+ className: "mx-auto w-full max-w-3xl",
9782
+ onSubmit: (event) => {
9783
+ event.preventDefault();
9784
+ submitDraft();
9785
+ },
9786
+ children: [
9787
+ error ? /* @__PURE__ */ jsxRuntime.jsx(ErrorBanner, { error, onRetry: retry, onReset: reset }) : null,
9788
+ staged.length > 0 ? /* @__PURE__ */ jsxRuntime.jsx(
9789
+ "ul",
9790
+ {
9791
+ "data-boff-explore": "attachments",
9792
+ className: "mb-2 flex flex-wrap gap-1.5",
9793
+ "aria-label": T.attach,
9794
+ children: staged.map((entry) => /* @__PURE__ */ jsxRuntime.jsxs(
9795
+ "li",
9796
+ {
9797
+ "data-boff-explore": "attachment",
9798
+ "data-text-extracted": entry.textExtracted ? "true" : "false",
9799
+ className: "flex max-w-full items-center gap-1.5 rounded-full border border-border bg-muted/60 py-1 pl-2.5 pr-1 text-xs",
9800
+ children: [
9801
+ /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Paperclip, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
9802
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate font-medium", title: entry.name, children: entry.name }),
9803
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "shrink-0 text-muted-foreground", children: [
9804
+ formatBytes(entry.size),
9805
+ entry.textExtracted ? "" : ` \xB7 ${T.attachmentNameOnly}`
9806
+ ] }),
9807
+ /* @__PURE__ */ jsxRuntime.jsx(
9808
+ "button",
9809
+ {
9810
+ type: "button",
9811
+ "data-boff-explore": "attachment-remove",
9812
+ onClick: () => removeAttachment(entry.id),
9813
+ "aria-label": fill(T.removeAttachment, { name: entry.name }),
9814
+ className: "ml-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-background hover:text-foreground",
9815
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-3 w-3" })
9816
+ }
9817
+ )
9818
+ ]
9819
+ },
9820
+ entry.id
9821
+ ))
9822
+ }
9823
+ ) : null,
9824
+ attachmentNotice !== null ? /* @__PURE__ */ jsxRuntime.jsx(
9825
+ "p",
9483
9826
  {
9484
- "data-boff-explore": "attachment",
9485
- "data-text-extracted": entry.textExtracted ? "true" : "false",
9486
- className: "flex max-w-full items-center gap-1.5 rounded-full border border-border bg-muted/60 py-1 pl-2.5 pr-1 text-xs",
9827
+ "data-boff-explore": "attachment-notice",
9828
+ role: "status",
9829
+ className: "mb-2 text-xs text-destructive",
9830
+ children: attachmentNotice
9831
+ }
9832
+ ) : null,
9833
+ /* @__PURE__ */ jsxRuntime.jsxs(
9834
+ "div",
9835
+ {
9836
+ className: cn(
9837
+ "flex min-w-0 items-end gap-2 rounded-2xl border bg-card p-2 shadow-sm transition-all",
9838
+ busy && "boff-explore-busy",
9839
+ overLimit ? "border-destructive/60" : "border-border focus-within:border-primary/40 focus-within:ring-1 focus-within:ring-ring"
9840
+ ),
9487
9841
  children: [
9488
- /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Paperclip, { className: "h-3 w-3 shrink-0 text-muted-foreground" }),
9489
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "truncate font-medium", title: entry.name, children: entry.name }),
9490
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "shrink-0 text-muted-foreground", children: [
9491
- formatBytes(entry.size),
9492
- entry.textExtracted ? "" : ` \xB7 ${T.attachmentNameOnly}`
9493
- ] }),
9494
9842
  /* @__PURE__ */ jsxRuntime.jsx(
9495
- "button",
9843
+ Textarea,
9496
9844
  {
9845
+ ref: textareaRef,
9846
+ value: draft,
9847
+ rows: 1,
9848
+ disabled: composerDisabled,
9849
+ onChange: (event) => setDraft(event.target.value),
9850
+ onKeyDown: handleKeyDown,
9851
+ onCompositionStart: () => {
9852
+ composingRef.current = true;
9853
+ },
9854
+ onCompositionEnd: () => {
9855
+ composingRef.current = false;
9856
+ },
9857
+ "aria-label": "Ask a question",
9858
+ placeholder: composerDisabled ? phase === "limited" ? "Message limit reached \u2014 please try again later" : "Explore is unavailable right now" : focusedProductName ? fill(T.placeholderProduct, {
9859
+ product: focusedProductName
9860
+ }) : T.placeholder,
9861
+ className: "max-h-[200px] min-h-[44px] w-full min-w-0 flex-1 resize-none border-0 bg-transparent px-2 py-2.5 text-base shadow-none focus-visible:ring-0 sm:text-sm"
9862
+ }
9863
+ ),
9864
+ /* @__PURE__ */ jsxRuntime.jsx(
9865
+ "input",
9866
+ {
9867
+ ref: fileInputRef,
9868
+ type: "file",
9869
+ multiple: true,
9870
+ className: "hidden",
9871
+ onChange: (event) => {
9872
+ void handleFilesPicked(event.target.files);
9873
+ event.target.value = "";
9874
+ }
9875
+ }
9876
+ ),
9877
+ /* @__PURE__ */ jsxRuntime.jsx(
9878
+ Button,
9879
+ {
9880
+ "data-boff-explore": "attach",
9881
+ type: "button",
9882
+ size: "icon",
9883
+ variant: "ghost",
9884
+ disabled: composerDisabled || staged.length >= MAX_ATTACHMENTS,
9885
+ "aria-label": T.attach,
9886
+ title: `${T.attach} \u2014 ${fill(T.attachHint, {
9887
+ max: String(MAX_ATTACHMENTS),
9888
+ size: String(MAX_ATTACHMENT_BYTES / (1024 * 1024))
9889
+ })}`,
9890
+ onClick: () => fileInputRef.current?.click(),
9891
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Paperclip, { className: "h-4 w-4" })
9892
+ }
9893
+ ),
9894
+ speech.supported ? /* @__PURE__ */ jsxRuntime.jsx(
9895
+ Button,
9896
+ {
9897
+ "data-boff-explore": "mic",
9898
+ "data-listening": speech.listening ? "true" : "false",
9497
9899
  type: "button",
9498
- "data-boff-explore": "attachment-remove",
9499
- onClick: () => removeAttachment(entry.id),
9500
- "aria-label": fill(T.removeAttachment, { name: entry.name }),
9501
- className: "ml-0.5 flex h-5 w-5 shrink-0 items-center justify-center rounded-full text-muted-foreground transition-colors hover:bg-background hover:text-foreground",
9502
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.X, { className: "h-3 w-3" })
9900
+ size: "icon",
9901
+ variant: speech.listening ? "default" : "ghost",
9902
+ disabled: composerDisabled,
9903
+ "aria-label": speech.listening ? T.stopDictating : T.dictate,
9904
+ "aria-pressed": speech.listening,
9905
+ title: speech.listening ? T.stopDictating : T.dictate,
9906
+ onClick: speech.toggle,
9907
+ className: cn(speech.listening && "animate-pulse"),
9908
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { className: "h-4 w-4" })
9909
+ }
9910
+ ) : null,
9911
+ busy ? /* @__PURE__ */ jsxRuntime.jsx(
9912
+ Button,
9913
+ {
9914
+ "data-boff-explore": "stop",
9915
+ type: "button",
9916
+ size: "icon",
9917
+ variant: "secondary",
9918
+ "aria-label": T.stop,
9919
+ onClick: stop,
9920
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, { className: "h-4 w-4" })
9921
+ }
9922
+ ) : /* @__PURE__ */ jsxRuntime.jsx(
9923
+ Button,
9924
+ {
9925
+ "data-boff-explore": "send",
9926
+ type: "submit",
9927
+ size: "icon",
9928
+ disabled: !canSubmit,
9929
+ "aria-label": T.send,
9930
+ children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { className: "h-4 w-4" })
9503
9931
  }
9504
9932
  )
9505
9933
  ]
9506
- },
9507
- entry.id
9508
- ))
9509
- }
9510
- ) : null,
9511
- attachmentNotice !== null ? /* @__PURE__ */ jsxRuntime.jsx(
9512
- "p",
9513
- {
9514
- "data-boff-explore": "attachment-notice",
9515
- role: "status",
9516
- className: "mb-2 text-xs text-destructive",
9517
- children: attachmentNotice
9518
- }
9519
- ) : null,
9520
- /* @__PURE__ */ jsxRuntime.jsxs(
9521
- "div",
9522
- {
9523
- className: cn(
9524
- "flex min-w-0 items-end gap-2 rounded-2xl border bg-card p-2 shadow-sm transition-all",
9525
- busy && "boff-explore-busy",
9526
- overLimit ? "border-destructive/60" : "border-border focus-within:border-primary/40 focus-within:ring-1 focus-within:ring-ring"
9934
+ }
9527
9935
  ),
9528
- children: [
9529
- /* @__PURE__ */ jsxRuntime.jsx(
9530
- Textarea,
9936
+ speech.listening || speech.interim !== "" ? /* @__PURE__ */ jsxRuntime.jsxs(
9937
+ "p",
9938
+ {
9939
+ "data-boff-explore": "dictation",
9940
+ className: "mt-2 flex items-center gap-2 text-xs text-muted-foreground",
9941
+ "aria-live": "polite",
9942
+ children: [
9943
+ /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative flex h-2 w-2 shrink-0", children: [
9944
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" }),
9945
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
9946
+ ] }),
9947
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "italic", children: speech.interim !== "" ? speech.interim : "Listening\u2026" })
9948
+ ]
9949
+ }
9950
+ ) : null,
9951
+ speech.error !== null ? /* @__PURE__ */ jsxRuntime.jsx(
9952
+ "p",
9953
+ {
9954
+ "data-boff-explore": "dictation-error",
9955
+ className: "mt-2 text-xs text-destructive",
9956
+ children: speech.error
9957
+ }
9958
+ ) : null,
9959
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
9960
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Enter to send \xB7 Shift + Enter for a new line" }),
9961
+ remainingToday !== null ? /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-boff-explore": "remaining", children: [
9962
+ remainingToday,
9963
+ " message",
9964
+ remainingToday === 1 ? "" : "s",
9965
+ " left today"
9966
+ ] }) : null,
9967
+ /* @__PURE__ */ jsxRuntime.jsxs(
9968
+ "span",
9531
9969
  {
9532
- ref: textareaRef,
9533
- value: draft,
9534
- rows: 1,
9535
- disabled: composerDisabled,
9536
- onChange: (event) => setDraft(event.target.value),
9537
- onKeyDown: handleKeyDown,
9538
- onCompositionStart: () => {
9539
- composingRef.current = true;
9540
- },
9541
- onCompositionEnd: () => {
9542
- composingRef.current = false;
9543
- },
9544
- "aria-label": "Ask a question",
9545
- placeholder: composerDisabled ? phase === "limited" ? "Message limit reached \u2014 please try again later" : "Explore is unavailable right now" : focusedProductName ? fill(T.placeholderProduct, {
9546
- product: focusedProductName
9547
- }) : T.placeholder,
9548
- className: "max-h-[200px] min-h-[44px] w-full min-w-0 flex-1 resize-none border-0 bg-transparent px-2 py-2.5 text-base shadow-none focus-visible:ring-0 sm:text-sm"
9970
+ className: cn(
9971
+ "ml-auto tabular-nums",
9972
+ overLimit && "font-medium text-destructive"
9973
+ ),
9974
+ children: [
9975
+ draft.length.toLocaleString(),
9976
+ " / ",
9977
+ maxLength.toLocaleString()
9978
+ ]
9549
9979
  }
9550
- ),
9980
+ )
9981
+ ] }),
9982
+ shareUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex items-center gap-2", children: [
9551
9983
  /* @__PURE__ */ jsxRuntime.jsx(
9552
9984
  "input",
9553
9985
  {
9554
- ref: fileInputRef,
9555
- type: "file",
9556
- multiple: true,
9557
- className: "hidden",
9558
- onChange: (event) => {
9559
- void handleFilesPicked(event.target.files);
9560
- event.target.value = "";
9561
- }
9986
+ "data-boff-explore": "share-url",
9987
+ readOnly: true,
9988
+ value: shareUrl,
9989
+ "aria-label": "Read-only link to this conversation",
9990
+ onFocus: (event) => event.currentTarget.select(),
9991
+ className: "min-w-0 flex-1 rounded-md border border-border bg-muted px-2 py-1 text-xs text-muted-foreground"
9562
9992
  }
9563
9993
  ),
9564
9994
  /* @__PURE__ */ jsxRuntime.jsx(
9565
- Button,
9995
+ "button",
9566
9996
  {
9567
- "data-boff-explore": "attach",
9568
9997
  type: "button",
9569
- size: "icon",
9570
- variant: "ghost",
9571
- disabled: composerDisabled || staged.length >= MAX_ATTACHMENTS,
9572
- "aria-label": T.attach,
9573
- title: `${T.attach} \u2014 ${fill(T.attachHint, {
9574
- max: String(MAX_ATTACHMENTS),
9575
- size: String(MAX_ATTACHMENT_BYTES / (1024 * 1024))
9576
- })}`,
9577
- onClick: () => fileInputRef.current?.click(),
9578
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Paperclip, { className: "h-4 w-4" })
9998
+ className: "shrink-0 text-xs underline underline-offset-2",
9999
+ onClick: () => setShareUrl(null),
10000
+ children: "Done"
9579
10001
  }
9580
- ),
9581
- speech.supported ? /* @__PURE__ */ jsxRuntime.jsx(
9582
- Button,
9583
- {
9584
- "data-boff-explore": "mic",
9585
- "data-listening": speech.listening ? "true" : "false",
9586
- type: "button",
9587
- size: "icon",
9588
- variant: speech.listening ? "default" : "ghost",
9589
- disabled: composerDisabled,
9590
- "aria-label": speech.listening ? T.stopDictating : T.dictate,
9591
- "aria-pressed": speech.listening,
9592
- title: speech.listening ? T.stopDictating : T.dictate,
9593
- onClick: speech.toggle,
9594
- className: cn(speech.listening && "animate-pulse"),
9595
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Mic, { className: "h-4 w-4" })
9596
- }
9597
- ) : null,
9598
- busy ? /* @__PURE__ */ jsxRuntime.jsx(
9599
- Button,
10002
+ )
10003
+ ] }) : null,
10004
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
10005
+ /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: T.disclaimerShort }),
10006
+ /* @__PURE__ */ jsxRuntime.jsx(
10007
+ "button",
9600
10008
  {
9601
- "data-boff-explore": "stop",
9602
10009
  type: "button",
9603
- size: "icon",
9604
- variant: "secondary",
9605
- "aria-label": T.stop,
9606
- onClick: stop,
9607
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.Square, { className: "h-4 w-4" })
9608
- }
9609
- ) : /* @__PURE__ */ jsxRuntime.jsx(
9610
- Button,
9611
- {
9612
- "data-boff-explore": "send",
9613
- type: "submit",
9614
- size: "icon",
9615
- disabled: !canSubmit,
9616
- "aria-label": T.send,
9617
- children: /* @__PURE__ */ jsxRuntime.jsx(lucideReact.ArrowUp, { className: "h-4 w-4" })
10010
+ "data-boff-explore": "disclaimer",
10011
+ onClick: () => setDisclaimerOpen(true),
10012
+ className: "shrink-0 underline underline-offset-2 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
10013
+ children: T.disclaimer
9618
10014
  }
9619
10015
  )
9620
- ]
9621
- }
9622
- ),
9623
- speech.listening || speech.interim !== "" ? /* @__PURE__ */ jsxRuntime.jsxs(
9624
- "p",
9625
- {
9626
- "data-boff-explore": "dictation",
9627
- className: "mt-2 flex items-center gap-2 text-xs text-muted-foreground",
9628
- "aria-live": "polite",
9629
- children: [
9630
- /* @__PURE__ */ jsxRuntime.jsxs("span", { className: "relative flex h-2 w-2 shrink-0", children: [
9631
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "absolute inline-flex h-full w-full animate-ping rounded-full bg-primary opacity-75" }),
9632
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "relative inline-flex h-2 w-2 rounded-full bg-primary" })
9633
- ] }),
9634
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "italic", children: speech.interim !== "" ? speech.interim : "Listening\u2026" })
9635
- ]
9636
- }
9637
- ) : null,
9638
- speech.error !== null ? /* @__PURE__ */ jsxRuntime.jsx(
9639
- "p",
9640
- {
9641
- "data-boff-explore": "dictation-error",
9642
- className: "mt-2 text-xs text-destructive",
9643
- children: speech.error
9644
- }
9645
- ) : null,
9646
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
9647
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "hidden sm:inline", children: "Enter to send \xB7 Shift + Enter for a new line" }),
9648
- remainingToday !== null ? /* @__PURE__ */ jsxRuntime.jsxs("span", { "data-boff-explore": "remaining", children: [
9649
- remainingToday,
9650
- " message",
9651
- remainingToday === 1 ? "" : "s",
9652
- " left today"
9653
- ] }) : null,
9654
- /* @__PURE__ */ jsxRuntime.jsxs(
9655
- "span",
9656
- {
9657
- className: cn(
9658
- "ml-auto tabular-nums",
9659
- overLimit && "font-medium text-destructive"
9660
- ),
9661
- children: [
9662
- draft.length.toLocaleString(),
9663
- " / ",
9664
- maxLength.toLocaleString()
9665
- ]
9666
- }
9667
- )
9668
- ] }),
9669
- shareUrl ? /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex items-center gap-2", children: [
9670
- /* @__PURE__ */ jsxRuntime.jsx(
9671
- "input",
9672
- {
9673
- "data-boff-explore": "share-url",
9674
- readOnly: true,
9675
- value: shareUrl,
9676
- "aria-label": "Read-only link to this conversation",
9677
- onFocus: (event) => event.currentTarget.select(),
9678
- className: "min-w-0 flex-1 rounded-md border border-border bg-muted px-2 py-1 text-xs text-muted-foreground"
9679
- }
9680
- ),
9681
- /* @__PURE__ */ jsxRuntime.jsx(
9682
- "button",
9683
- {
9684
- type: "button",
9685
- className: "shrink-0 text-xs underline underline-offset-2",
9686
- onClick: () => setShareUrl(null),
9687
- children: "Done"
9688
- }
9689
- )
9690
- ] }) : null,
9691
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "mt-2 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-muted-foreground", children: [
9692
- /* @__PURE__ */ jsxRuntime.jsx("span", { className: "min-w-0 truncate", children: T.disclaimerShort }),
9693
- /* @__PURE__ */ jsxRuntime.jsx(
9694
- "button",
9695
- {
9696
- type: "button",
9697
- "data-boff-explore": "disclaimer",
9698
- onClick: () => setDisclaimerOpen(true),
9699
- className: "shrink-0 underline underline-offset-2 transition-colors hover:text-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring",
9700
- children: T.disclaimer
9701
- }
9702
- )
9703
- ] })
9704
- ]
10016
+ ] })
10017
+ ]
10018
+ }
10019
+ )
9705
10020
  }
9706
- ) })
10021
+ )
9707
10022
  ]
9708
10023
  }
9709
10024
  );