@opengeni/react 0.37.0 → 0.40.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/README.md +88 -2
  2. package/dist/{chunk-7CJOAW3V.js → chunk-EB67J347.js} +1 -135
  3. package/dist/chunk-EB67J347.js.map +1 -0
  4. package/dist/chunk-EKZH6IDG.js +141 -0
  5. package/dist/chunk-EKZH6IDG.js.map +1 -0
  6. package/dist/{chunk-FSPDND3P.js → chunk-JALF5FI3.js} +8 -6
  7. package/dist/{chunk-FSPDND3P.js.map → chunk-JALF5FI3.js.map} +1 -1
  8. package/dist/{chunk-UCZPNXV3.js → chunk-KR2SK5GJ.js} +1292 -711
  9. package/dist/chunk-KR2SK5GJ.js.map +1 -0
  10. package/dist/{chunk-IP22SLO6.js → chunk-OMCFRWHL.js} +28 -441
  11. package/dist/chunk-OMCFRWHL.js.map +1 -0
  12. package/dist/{chunk-HJ4OQVGW.js → chunk-Q2NCKWTK.js} +10 -5
  13. package/dist/chunk-Q2NCKWTK.js.map +1 -0
  14. package/dist/{chunk-ALA3UGWB.js → chunk-SXIQK54Z.js} +8 -6
  15. package/dist/{chunk-ALA3UGWB.js.map → chunk-SXIQK54Z.js.map} +1 -1
  16. package/dist/chunk-WZT5G5OR.js +433 -0
  17. package/dist/chunk-WZT5G5OR.js.map +1 -0
  18. package/dist/client.d.ts +2 -0
  19. package/dist/components/chat-composer.d.ts +5 -1
  20. package/dist/components/human-input-form.d.ts +16 -6
  21. package/dist/components/human-input-surface.d.ts +19 -0
  22. package/dist/components/machine-input-display.d.ts +12 -0
  23. package/dist/components/message-timeline.d.ts +6 -1
  24. package/dist/components/session-chrome.d.ts +1 -0
  25. package/dist/components/tip-follow.d.ts +30 -0
  26. package/dist/composer.js +14 -11
  27. package/dist/hooks/use-composer.d.ts +1 -1
  28. package/dist/index.d.ts +4 -0
  29. package/dist/index.js +2246 -2236
  30. package/dist/index.js.map +1 -1
  31. package/dist/machines.js +4 -3
  32. package/dist/realtime/dropdown-menu.d.ts +16 -0
  33. package/dist/realtime/realtime-control.d.ts +126 -0
  34. package/dist/realtime.d.ts +11 -0
  35. package/dist/realtime.js +1023 -0
  36. package/dist/realtime.js.map +1 -0
  37. package/dist/session-context.d.ts +7 -1
  38. package/dist/session-ui.d.ts +2 -0
  39. package/dist/session-ui.js +5 -3
  40. package/dist/session.js +4 -3
  41. package/dist/timeline/compute-label.d.ts +2 -0
  42. package/dist/timeline/index.d.ts +1 -0
  43. package/package.json +7 -2
  44. package/src/client.ts +13 -0
  45. package/src/components/chat-composer.tsx +11 -2
  46. package/src/components/composer-transcription-control.tsx +2 -2
  47. package/src/components/composer.tsx +14 -5
  48. package/src/components/human-input-form.tsx +506 -186
  49. package/src/components/human-input-surface.tsx +80 -0
  50. package/src/components/machine-input-display.ts +83 -0
  51. package/src/components/message-timeline.tsx +348 -308
  52. package/src/components/model-policy-picker.tsx +8 -3
  53. package/src/components/sandbox-files.tsx +1 -1
  54. package/src/components/session-chrome.tsx +3 -1
  55. package/src/components/tip-follow.ts +127 -6
  56. package/src/hooks/use-composer.ts +16 -13
  57. package/src/index.ts +4 -0
  58. package/src/realtime/dropdown-menu.tsx +123 -0
  59. package/src/realtime/realtime-control.tsx +1178 -0
  60. package/src/realtime.ts +27 -0
  61. package/src/session-context.ts +16 -0
  62. package/src/session-ui.ts +2 -0
  63. package/src/timeline/compute-label.tsx +18 -0
  64. package/src/timeline/index.ts +3 -0
  65. package/src/timeline/tool-renderers.tsx +232 -43
  66. package/styles/index.css +61 -0
  67. package/styles/tokens.css +2 -0
  68. package/dist/chunk-7CJOAW3V.js.map +0 -1
  69. package/dist/chunk-HJ4OQVGW.js.map +0 -1
  70. package/dist/chunk-IP22SLO6.js.map +0 -1
  71. package/dist/chunk-UCZPNXV3.js.map +0 -1
@@ -452,6 +452,11 @@ export function ModelPolicyPickerMenu(
452
452
  <p className="px-2.5 pt-1 pb-1 text-xs font-medium tracking-wide text-og-fg-subtle uppercase">
453
453
  {messages.thinking}
454
454
  </p>
455
+ {focusModel.catalog.capabilities?.inputModalities.includes("image") === false ? (
456
+ <p className="px-2.5 pb-1.5 text-xs leading-relaxed text-og-fg-subtle">
457
+ Unsupported attachments stay in the session but are hidden from this model.
458
+ </p>
459
+ ) : null}
455
460
  <div className="flex flex-col gap-0.5">
456
461
  {effortOptionsForModel(focusModel.catalog).map((effort) => {
457
462
  const selected = activeModel && effort === props.effort;
@@ -540,7 +545,7 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
540
545
  disabled={props.disabled}
541
546
  aria-label={messages.label}
542
547
  className={cn(
543
- "inline-flex h-8 max-w-64 items-center gap-1 rounded-full border border-transparent px-2.5 text-xs text-og-fg-muted outline-none transition-colors hover:border-og-border hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:cursor-not-allowed disabled:opacity-50",
548
+ "inline-flex h-8 max-w-64 items-center gap-1 rounded-full border border-transparent px-2.5 text-xs text-og-fg-muted outline-none transition-colors hover:border-og-border hover:bg-og-surface-2 hover:text-og-fg focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:cursor-not-allowed disabled:opacity-50 max-sm:h-7 max-sm:max-w-[9rem] max-sm:px-2",
544
549
  props.className,
545
550
  )}
546
551
  >
@@ -552,10 +557,10 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
552
557
  className="text-og-fg"
553
558
  />
554
559
  <span className="truncate font-medium text-og-fg">{selected?.label ?? props.model}</span>
555
- <span>{labelReasoningEffort(props.effort)}</span>
560
+ <span className="max-sm:hidden">{labelReasoningEffort(props.effort)}</span>
556
561
  {props.latencyMode === "fast" ? (
557
562
  <ZapIcon
558
- className="size-3.5 shrink-0 text-og-fg"
563
+ className="size-3.5 shrink-0 fill-current stroke-current text-og-fg"
559
564
  aria-label={messages.fast}
560
565
  data-testid="model-picker-fast-icon"
561
566
  />
@@ -382,7 +382,7 @@ function WakeButton({ children, onClick }: { children: ReactNode; onClick: () =>
382
382
  <button
383
383
  type="button"
384
384
  onClick={onClick}
385
- className="mt-1 inline-flex min-h-11 items-center justify-center rounded-og-md bg-og-accent px-3 py-2 text-og-sm font-medium text-og-on-accent shadow-sm transition-colors hover:bg-og-accent-hover focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg"
385
+ className="mt-1 inline-flex min-h-11 items-center justify-center rounded-og-md bg-og-accent-deep px-3 py-2 text-og-sm font-medium text-og-accent-fg shadow-sm transition-opacity hover:opacity-90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-og-accent focus-visible:ring-offset-2 focus-visible:ring-offset-og-bg"
386
386
  >
387
387
  {children}
388
388
  </button>
@@ -18,6 +18,7 @@
18
18
  * | `--og-session-chrome-chip-pad-x` / `--og-session-chrome-chip-gap` | Chip padding / rail gap |
19
19
  * | `--og-session-chrome-rail-pad` | Outer rail inset |
20
20
  * | `--og-session-chrome-panel-pad-x` / `-y` | Expanded panel padding |
21
+ * | `--og-session-chrome-panel-max-height` | Cap for expanded body (scrolls inside) |
21
22
  * | `--og-session-chrome-duration` / `--og-session-chrome-ease` | Expand + pill motion |
22
23
  * | `--og-session-chrome-crossfade-duration` | Segment content opacity crossfade |
23
24
  * | `--og-session-chrome-row-hover` | Inbox / queue row hover wash |
@@ -664,8 +665,9 @@ export function SessionChrome({
664
665
  >
665
666
  <div
666
667
  ref={panelBodyRef}
667
- className="relative border-t border-og-border/50"
668
+ className="relative overflow-y-auto overscroll-contain border-t border-og-border/50"
668
669
  style={{
670
+ maxHeight: "var(--og-session-chrome-panel-max-height)",
669
671
  paddingInline: "var(--og-session-chrome-panel-pad-x)",
670
672
  paddingBlock: "var(--og-session-chrome-panel-pad-y)",
671
673
  }}
@@ -81,6 +81,13 @@ export const TIP_FOLLOW_VELOCITY_ARM_DEBT_PX = 72;
81
81
  export const TIP_FOLLOW_VELOCITY_DECAY_MS = _t(180);
82
82
  /** Reader-up pixels above clamp budget that count as leaving the tip. */
83
83
  export const TIP_FOLLOW_READER_UP_EPS_PX = 2;
84
+ /**
85
+ * Browsers snap scrollTop writes to device pixels (whole px at dpr 1). A DOM
86
+ * position within this window of the camera's own fractional position is that
87
+ * quantization echo, not external motion — keep the fraction so sub-pixel
88
+ * settle steps accumulate. Anything larger (clamp, user, glue) re-bases.
89
+ */
90
+ export const TIP_FOLLOW_QUANTIZE_WINDOW_PX = 1;
84
91
 
85
92
  /** Test-only override for scrollend feature detection (`null` = probe DOM). */
86
93
  let scrollEndSupportOverride: boolean | null = null;
@@ -122,11 +129,20 @@ export type TipFollowState = {
122
129
  hotUntil: number;
123
130
  lastTs: number;
124
131
  lastHeight: number;
132
+ /** Last observed scroller clientHeight — viewport shrink heats like tip growth. */
133
+ lastClientHeight: number;
125
134
  lastGrowthAt: number;
126
135
  /** EMA of recent height growth (px/s); nudges τ / ceiling under bursts. */
127
136
  growthVelocity: number;
128
137
  /** Camera velocity toward tip (px/s). Smoothed — never jumps with debt. */
129
138
  scrollVelocity: number;
139
+ /**
140
+ * Fractional camera position. The DOM quantizes scrollTop to device pixels,
141
+ * so settle steps under 1px would be discarded every frame if the camera
142
+ * re-based on the DOM — it then parks 20-50px short of the tip forever.
143
+ * `null` = no fraction in flight; adopt the DOM position.
144
+ */
145
+ cameraTop: number | null;
130
146
  };
131
147
 
132
148
  export type TipFollowStepInput = {
@@ -150,9 +166,11 @@ export function createTipFollowState(): TipFollowState {
150
166
  hotUntil: 0,
151
167
  lastTs: 0,
152
168
  lastHeight: 0,
169
+ lastClientHeight: 0,
153
170
  lastGrowthAt: 0,
154
171
  growthVelocity: 0,
155
172
  scrollVelocity: 0,
173
+ cameraTop: null,
156
174
  };
157
175
  }
158
176
 
@@ -162,6 +180,7 @@ export function tipFollowCancel(state: TipFollowState): TipFollowState {
162
180
  running: false,
163
181
  lastTs: 0,
164
182
  scrollVelocity: 0,
183
+ cameraTop: null,
165
184
  };
166
185
  }
167
186
 
@@ -196,6 +215,49 @@ export function tipFollowNoteGrowth(
196
215
  };
197
216
  }
198
217
 
218
+ /**
219
+ * Record scroller viewport height. A shrink (composer / SessionChrome /
220
+ * window) raises tip debt (`scrollHeight - clientHeight`) without growing
221
+ * content — the same debt content-growth would create. Heat the camera so
222
+ * catch-up does not fall into cold soft-settle (~42px/s) and leave the tip
223
+ * parked under the chrome.
224
+ */
225
+ export function tipFollowNoteViewportShrink(
226
+ state: TipFollowState,
227
+ clientHeight: number,
228
+ now: number,
229
+ ): TipFollowState {
230
+ const previous = state.lastClientHeight;
231
+ if (previous <= 0) {
232
+ return { ...state, lastClientHeight: clientHeight };
233
+ }
234
+ const shrink = previous - clientHeight;
235
+ if (shrink <= 0) {
236
+ return { ...state, lastClientHeight: clientHeight };
237
+ }
238
+ if (shrink <= TIP_FOLLOW_SHRINK_EPS_PX) {
239
+ // Sub-eps shrink: HOLD the baseline (deadband with memory). An animated
240
+ // chrome/composer ease-out delivers many ≤eps frames; adopting each one
241
+ // leaked the whole tail into cold debt with no glue and no heat.
242
+ return state;
243
+ }
244
+ const dt = state.lastGrowthAt > 0 ? now - state.lastGrowthAt : 0;
245
+ let growthVelocity = state.growthVelocity;
246
+ if (dt > 0) {
247
+ const instant = (shrink / dt) * 1000;
248
+ growthVelocity = growthVelocity * 0.65 + instant * 0.35;
249
+ } else {
250
+ growthVelocity = Math.max(growthVelocity, shrink * (1000 / TIP_FOLLOW_HOT_IDLE_MS));
251
+ }
252
+ return {
253
+ ...state,
254
+ lastClientHeight: clientHeight,
255
+ lastGrowthAt: now,
256
+ growthVelocity,
257
+ hotUntil: now + TIP_FOLLOW_HOT_IDLE_MS,
258
+ };
259
+ }
260
+
199
261
  /**
200
262
  * Adaptive τ: small debt → calm; large debt / armed velocity → catch-up.
201
263
  * While hot, floor urgency so live streams are not stuck on calm τ.
@@ -376,6 +438,29 @@ export function tipFollowCompensateShrink(
376
438
  return Math.max(0, Math.min(maxScroll, scrollTop - delta));
377
439
  }
378
440
 
441
+ /**
442
+ * Viewport shrank (SessionChrome / composer / window). maxScroll rises by ≈Δc;
443
+ * keep the same tip distance: scrollTop += Δc. Without this, pinned follow only
444
+ * sees new tip debt and soft-settles under the chrome.
445
+ */
446
+ export function tipFollowCompensateViewportShrink(
447
+ scrollTop: number,
448
+ previousClientHeight: number,
449
+ nextClientHeight: number,
450
+ scrollHeight: number,
451
+ epsPx: number = TIP_FOLLOW_SHRINK_EPS_PX,
452
+ ): number {
453
+ if (previousClientHeight <= 0) {
454
+ return scrollTop;
455
+ }
456
+ const shrink = previousClientHeight - nextClientHeight;
457
+ if (shrink <= epsPx) {
458
+ return scrollTop;
459
+ }
460
+ const maxScroll = Math.max(0, scrollHeight - nextClientHeight);
461
+ return Math.max(0, Math.min(maxScroll, scrollTop + shrink));
462
+ }
463
+
379
464
  /**
380
465
  * One camera step. Shell schedules the next rAF while `result.state.running`.
381
466
  */
@@ -385,12 +470,39 @@ export function tipFollowStep(
385
470
  ): TipFollowStepResult {
386
471
  const { scrollTop, scrollHeight, clientHeight, now, pinned, reducedMotion, revealed } = input;
387
472
  const previousHeight = state.lastHeight;
473
+ const previousClient = state.lastClientHeight;
388
474
  const frameGrowth =
389
475
  previousHeight > 0 && scrollHeight > previousHeight ? scrollHeight - previousHeight : 0;
390
- const grew = frameGrowth > 0;
476
+ const frameViewportShrink =
477
+ previousClient > 0 && clientHeight < previousClient - TIP_FOLLOW_SHRINK_EPS_PX
478
+ ? previousClient - clientHeight
479
+ : 0;
480
+ const grew = frameGrowth > 0 || frameViewportShrink > 0;
391
481
  let noted = tipFollowNoteGrowth(state, scrollHeight, now);
482
+ noted = tipFollowNoteViewportShrink(noted, clientHeight, now);
483
+ // Sub-device-pixel writes are floored by the engine: a DOM position within
484
+ // the quantization window of our own fractional camera is the echo of our
485
+ // last write, not external motion — keep the fraction so settle steps
486
+ // accumulate. Larger disagreement (clamp / reader / snap) re-bases.
487
+ let cameraTop = scrollTop;
488
+ if (
489
+ state.cameraTop !== null &&
490
+ Math.abs(state.cameraTop - scrollTop) <= TIP_FOLLOW_QUANTIZE_WINDOW_PX
491
+ ) {
492
+ cameraTop = state.cameraTop;
493
+ }
494
+ // Chrome/composer dock: tip-glue before ease so debt from ΔclientHeight is
495
+ // not left for cold soft-settle (content still under the new bar).
496
+ if (pinned && frameViewportShrink > 0) {
497
+ cameraTop = tipFollowCompensateViewportShrink(
498
+ cameraTop,
499
+ previousClient,
500
+ clientHeight,
501
+ scrollHeight,
502
+ );
503
+ }
392
504
  const target = targetScrollTop(scrollHeight, clientHeight);
393
- const debt = target - scrollTop;
505
+ const debt = target - cameraTop;
394
506
  const hot = now < noted.hotUntil;
395
507
  const settling = !hot && Math.abs(debt) < TIP_FOLLOW_CATCHUP_DEBT_PX;
396
508
 
@@ -426,7 +538,7 @@ export function tipFollowStep(
426
538
 
427
539
  if (Math.abs(debt) <= 0.75) {
428
540
  return {
429
- scrollTop: Math.abs(debt) > 0 ? target : scrollTop,
541
+ scrollTop: Math.abs(debt) > 0 ? target : cameraTop,
430
542
  state: {
431
543
  ...tipFollowCancel(noted),
432
544
  growthVelocity: hot ? noted.growthVelocity : 0,
@@ -445,13 +557,19 @@ export function tipFollowStep(
445
557
  let scrollVelocity =
446
558
  (noted.scrollVelocity ?? 0) + (desiredVel - (noted.scrollVelocity ?? 0)) * velAlpha;
447
559
 
448
- const maxStep = tipFollowMaxStepPx(debt, noted.growthVelocity, dt, frameGrowth, hot);
560
+ const maxStep = tipFollowMaxStepPx(
561
+ debt,
562
+ noted.growthVelocity,
563
+ dt,
564
+ frameGrowth + frameViewportShrink,
565
+ hot,
566
+ );
449
567
  const maxVel = maxStep / dtSec;
450
568
  if (Math.abs(scrollVelocity) > maxVel) {
451
569
  scrollVelocity = Math.sign(scrollVelocity) * maxVel;
452
570
  }
453
571
 
454
- let next = scrollTop + scrollVelocity * dtSec;
572
+ let next = cameraTop + scrollVelocity * dtSec;
455
573
  // Never overshoot the tip — stop and kill velocity on arrival.
456
574
  if (debt > 0 && next >= target - 0.35) {
457
575
  next = target;
@@ -462,13 +580,16 @@ export function tipFollowStep(
462
580
  }
463
581
  next = Math.max(0, next);
464
582
 
583
+ const running = Math.abs(target - next) > 0.35 || Math.abs(scrollVelocity) > 1;
465
584
  return {
466
585
  scrollTop: next,
467
586
  state: {
468
587
  ...noted,
469
588
  scrollVelocity,
470
- running: Math.abs(target - next) > 0.35 || Math.abs(scrollVelocity) > 1,
589
+ running,
471
590
  lastTs: now,
591
+ // Carry the fraction the DOM will floor away; drop it once landed.
592
+ cameraTop: running ? next : null,
472
593
  },
473
594
  };
474
595
  }
@@ -1,12 +1,13 @@
1
- import type {
2
- ComposerDraft,
3
- EffectiveControlResumeOption,
4
- EffectiveSessionControl,
5
- OpenGeniApiError,
6
- ResourceRef,
7
- SaveComposerDraftRequest,
8
- SendMessageInput,
9
- SessionEvent,
1
+ import {
2
+ DEFAULT_FILE_RESOURCE_MOUNT_ROOT,
3
+ type ComposerDraft,
4
+ type EffectiveControlResumeOption,
5
+ type EffectiveSessionControl,
6
+ type OpenGeniApiError,
7
+ type ResourceRef,
8
+ type SaveComposerDraftRequest,
9
+ type SendMessageInput,
10
+ type SessionEvent,
10
11
  } from "@opengeni/sdk";
11
12
  import { useCallback, useEffect, useLayoutEffect, useRef, useState } from "react";
12
13
  import { useEmbeddedSession, type EmbeddedSessionClientOverride } from "../session-context";
@@ -537,13 +538,15 @@ export function useComposer(
537
538
  replaceLocal ||
538
539
  (!localWasDirtyAtStart && localAtStart === localEditRevision.current)
539
540
  ) {
540
- // Model/effort ride in sendExtras (outside localEditRevision). If
541
+ // Model/effort/latency ride in sendExtras (outside
542
+ // localEditRevision). If
541
543
  // the picker changed during this fetch, skip onDraftApplied so a
542
- // stale server model/effort cannot undo the operator's pick.
544
+ // stale server policy cannot undo the operator's pick.
543
545
  const extrasNow = resolveSendExtras(sendExtrasRef.current);
544
546
  const pickerChangedDuringFetch =
545
547
  extrasNow.model !== extrasAtStart.model ||
546
- extrasNow.reasoningEffort !== extrasAtStart.reasoningEffort;
548
+ extrasNow.reasoningEffort !== extrasAtStart.reasoningEffort ||
549
+ extrasNow.latencyMode !== extrasAtStart.latencyMode;
547
550
  valueRef.current = fetched.text;
548
551
  restoredResourcesRef.current = fetched.resources;
549
552
  lastSavedSignature.current = draftSignature(draftPayload(fetched));
@@ -1442,7 +1445,7 @@ function mergeResources(base: ResourceRef[], additions: ResourceRef[]): Resource
1442
1445
  // different duplicate counts after server normalization.
1443
1446
  const key =
1444
1447
  resource.kind === "file"
1445
- ? `file:${resource.fileId}\u0000${resource.mountPath ?? `files/${resource.fileId}`}`
1448
+ ? `file:${resource.fileId}\u0000${resource.mountPath ?? `${DEFAULT_FILE_RESOURCE_MOUNT_ROOT}/${resource.fileId}`}`
1446
1449
  : JSON.stringify(resource);
1447
1450
  if (seen.has(key)) return false;
1448
1451
  seen.add(key);
package/src/index.ts CHANGED
@@ -256,6 +256,8 @@ export type {
256
256
  HumanInputFormMessages,
257
257
  HumanInputFormProps,
258
258
  } from "./components/human-input-form";
259
+ export { HumanInputSurface } from "./components/human-input-surface";
260
+ export type { HumanInputSurfaceProps } from "./components/human-input-surface";
259
261
 
260
262
  // Timeline projection
261
263
  export {
@@ -445,6 +447,8 @@ export { SandboxTerminal } from "./components/sandbox-terminal";
445
447
  export type { SandboxTerminalProps, XtermTheme } from "./components/sandbox-terminal";
446
448
  export { FileBrowser } from "./components/file-browser";
447
449
  export type { FileBrowserProps } from "./components/file-browser";
450
+ export { WorkbenchChanges } from "./components/workbench-changes";
451
+ export type { WorkbenchChangesProps } from "./components/workbench-changes";
448
452
  export { DiffView } from "./components/diff-view";
449
453
  export type { DiffViewProps, DiffTheme } from "./components/diff-view";
450
454
  export { PierreDiff } from "./components/pierre-diff";
@@ -0,0 +1,123 @@
1
+ "use client";
2
+
3
+ import { ChevronRightIcon } from "lucide-react";
4
+ import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
5
+ import type { ComponentProps } from "react";
6
+ import { cn } from "../lib/cn";
7
+
8
+ function DropdownMenu(props: ComponentProps<typeof DropdownMenuPrimitive.Root>) {
9
+ return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
10
+ }
11
+
12
+ function DropdownMenuTrigger(props: ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
13
+ return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
14
+ }
15
+
16
+ function DropdownMenuContent({
17
+ className,
18
+ sideOffset = 4,
19
+ ...props
20
+ }: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
21
+ return (
22
+ <DropdownMenuPrimitive.Portal>
23
+ <DropdownMenuPrimitive.Content
24
+ data-slot="dropdown-menu-content"
25
+ sideOffset={sideOffset}
26
+ className={cn(
27
+ "z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
28
+ className,
29
+ )}
30
+ {...props}
31
+ />
32
+ </DropdownMenuPrimitive.Portal>
33
+ );
34
+ }
35
+
36
+ function DropdownMenuItem({
37
+ className,
38
+ inset,
39
+ variant = "default",
40
+ ...props
41
+ }: ComponentProps<typeof DropdownMenuPrimitive.Item> & {
42
+ inset?: boolean;
43
+ variant?: "default" | "destructive";
44
+ }) {
45
+ return (
46
+ <DropdownMenuPrimitive.Item
47
+ data-slot="dropdown-menu-item"
48
+ data-inset={inset}
49
+ data-variant={variant}
50
+ className={cn(
51
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
52
+ className,
53
+ )}
54
+ {...props}
55
+ />
56
+ );
57
+ }
58
+
59
+ function DropdownMenuSeparator({
60
+ className,
61
+ ...props
62
+ }: ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
63
+ return (
64
+ <DropdownMenuPrimitive.Separator
65
+ data-slot="dropdown-menu-separator"
66
+ className={cn("-mx-1 my-1 h-px bg-border", className)}
67
+ {...props}
68
+ />
69
+ );
70
+ }
71
+
72
+ function DropdownMenuSub(props: ComponentProps<typeof DropdownMenuPrimitive.Sub>) {
73
+ return <DropdownMenuPrimitive.Sub data-slot="dropdown-menu-sub" {...props} />;
74
+ }
75
+
76
+ function DropdownMenuSubTrigger({
77
+ className,
78
+ inset,
79
+ children,
80
+ ...props
81
+ }: ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }) {
82
+ return (
83
+ <DropdownMenuPrimitive.SubTrigger
84
+ data-slot="dropdown-menu-sub-trigger"
85
+ data-inset={inset}
86
+ className={cn(
87
+ "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
88
+ className,
89
+ )}
90
+ {...props}
91
+ >
92
+ {children}
93
+ <ChevronRightIcon className="ml-auto size-4" />
94
+ </DropdownMenuPrimitive.SubTrigger>
95
+ );
96
+ }
97
+
98
+ function DropdownMenuSubContent({
99
+ className,
100
+ ...props
101
+ }: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
102
+ return (
103
+ <DropdownMenuPrimitive.SubContent
104
+ data-slot="dropdown-menu-sub-content"
105
+ className={cn(
106
+ "z-50 min-w-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
107
+ className,
108
+ )}
109
+ {...props}
110
+ />
111
+ );
112
+ }
113
+
114
+ export {
115
+ DropdownMenu,
116
+ DropdownMenuContent,
117
+ DropdownMenuItem,
118
+ DropdownMenuSeparator,
119
+ DropdownMenuSub,
120
+ DropdownMenuSubContent,
121
+ DropdownMenuSubTrigger,
122
+ DropdownMenuTrigger,
123
+ };