@opengeni/react 0.44.3 → 0.44.5

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 (50) hide show
  1. package/README.md +42 -0
  2. package/dist/chunk-22KP6LR5.js +31 -0
  3. package/dist/{chunk-TK7G6XLT.js.map → chunk-22KP6LR5.js.map} +1 -1
  4. package/dist/{chunk-SXIQK54Z.js → chunk-6EDV6YFE.js} +8 -8
  5. package/dist/{chunk-SXIQK54Z.js.map → chunk-6EDV6YFE.js.map} +1 -1
  6. package/dist/{chunk-U6K24XQD.js → chunk-EHSYZ4KP.js} +37 -16
  7. package/dist/chunk-EHSYZ4KP.js.map +1 -0
  8. package/dist/chunk-JB5725BI.js +48 -0
  9. package/dist/chunk-JB5725BI.js.map +1 -0
  10. package/dist/{chunk-QQRM3DO3.js → chunk-L3EWO3UK.js} +6 -6
  11. package/dist/{chunk-QQRM3DO3.js.map → chunk-L3EWO3UK.js.map} +1 -1
  12. package/dist/{chunk-YNYIAYXQ.js → chunk-LYRJUU5V.js} +23 -23
  13. package/dist/chunk-LYRJUU5V.js.map +1 -0
  14. package/dist/{chunk-ATSTR5P3.js → chunk-U3CSCMWA.js} +5 -49
  15. package/dist/chunk-U3CSCMWA.js.map +1 -0
  16. package/dist/{chunk-YDNWMKXO.js → chunk-VCBFUSUZ.js} +4 -4
  17. package/dist/chunk-VCBFUSUZ.js.map +1 -0
  18. package/dist/components/model-policy-picker.d.ts +10 -1
  19. package/dist/composer.js +5 -4
  20. package/dist/index.js +11 -9
  21. package/dist/index.js.map +1 -1
  22. package/dist/machines.js +2 -2
  23. package/dist/{queue-surface-implementation-THPAKGMD.js → queue-surface-implementation-J4XJFKRW.js} +41 -26
  24. package/dist/queue-surface-implementation-J4XJFKRW.js.map +1 -0
  25. package/dist/realtime/dropdown-menu.d.ts +4 -4
  26. package/dist/realtime.js +58 -20
  27. package/dist/realtime.js.map +1 -1
  28. package/dist/session-ui.js +5 -4
  29. package/package.json +1 -1
  30. package/src/components/composer.tsx +3 -3
  31. package/src/components/enrollment-device-flow.tsx +2 -2
  32. package/src/components/message-timeline.tsx +15 -15
  33. package/src/components/model-policy-picker.tsx +47 -14
  34. package/src/components/queue-surface-implementation.tsx +28 -18
  35. package/src/components/queue-surface-state.tsx +3 -3
  36. package/src/components/queue-surface.tsx +2 -2
  37. package/src/components/session-status.tsx +1 -1
  38. package/src/lib/cn.ts +14 -1
  39. package/src/lib/use-portal-token-style.ts +1 -1
  40. package/src/provider.tsx +4 -4
  41. package/src/realtime/dropdown-menu.tsx +51 -8
  42. package/src/realtime/realtime-control.tsx +2 -2
  43. package/styles/index.css +91 -11
  44. package/styles/tokens.css +61 -0
  45. package/dist/chunk-ATSTR5P3.js.map +0 -1
  46. package/dist/chunk-TK7G6XLT.js +0 -18
  47. package/dist/chunk-U6K24XQD.js.map +0 -1
  48. package/dist/chunk-YDNWMKXO.js.map +0 -1
  49. package/dist/chunk-YNYIAYXQ.js.map +0 -1
  50. package/dist/queue-surface-implementation-THPAKGMD.js.map +0 -1
@@ -9,8 +9,17 @@ import {
9
9
  } from "lucide-react";
10
10
  import { motion, useReducedMotion } from "motion/react";
11
11
  import { DropdownMenu } from "radix-ui";
12
- import { useEffect, useMemo, useState, type ReactNode, type SVGProps } from "react";
12
+ import {
13
+ useEffect,
14
+ useMemo,
15
+ useRef,
16
+ useState,
17
+ type CSSProperties,
18
+ type ReactNode,
19
+ type SVGProps,
20
+ } from "react";
13
21
  import { cn } from "../lib/cn";
22
+ import { usePortalTokenStyle } from "../lib/use-portal-token-style";
14
23
  import {
15
24
  coerceReasoningEffortForModel,
16
25
  effortOptionsForModel,
@@ -72,12 +81,21 @@ export type ModelPolicyPickerProps = {
72
81
  disabled?: boolean | undefined;
73
82
  loading?: boolean | undefined;
74
83
  error?: string | null | undefined;
84
+ /** Controlled menu state. Omit for the built-in uncontrolled behavior. */
85
+ open?: boolean | undefined;
86
+ /** Initial menu state when `open` is uncontrolled. */
87
+ defaultOpen?: boolean | undefined;
88
+ onOpenChange?: ((open: boolean) => void) | undefined;
75
89
  /** Non-Codex models stay visible but cannot be selected. */
76
90
  codexOnly?: boolean | undefined;
77
91
  /** Resets drill-down navigation when the session scope changes. */
78
92
  sessionKey?: string | undefined;
79
93
  /** Prefer bottom on new-chat surfaces and top for bottom-docked composers. */
80
94
  menuSide?: "top" | "bottom" | undefined;
95
+ /** Classes for the portalled menu surface. Prefer --og-* tokens for theming. */
96
+ contentClassName?: string | undefined;
97
+ /** Inline styles for the portalled menu, applied after inherited --og-* tokens. */
98
+ contentStyle?: CSSProperties | undefined;
81
99
  className?: string | undefined;
82
100
  messages?: Partial<ModelPolicyPickerMessages> | undefined;
83
101
  onModelChange: (modelId: string) => void;
@@ -225,17 +243,19 @@ export function PickerNavRow(props: {
225
243
  onClick={props.onClick}
226
244
  data-testid={props.testId}
227
245
  className={cn(
228
- "flex w-full cursor-pointer items-center gap-2 rounded-og-sm px-2.5 py-2 text-left text-og-fg outline-none transition-colors hover:bg-og-surface-2 focus-visible:ring-2 focus-visible:ring-og-accent/40",
246
+ "flex w-full cursor-pointer items-center gap-2 rounded-og-sm px-[var(--og-model-picker-row-padding-x)] py-[var(--og-model-picker-row-padding-y)] text-left text-og-fg outline-none transition-colors hover:bg-og-surface-2 focus-visible:ring-2 focus-visible:ring-og-accent/40",
229
247
  props.disabled && "cursor-not-allowed opacity-50",
230
248
  )}
231
249
  >
232
250
  {props.icon}
233
251
  <span className="min-w-0 flex-1">
234
- <span className={cn("block truncate text-sm", props.active && "font-medium")}>
252
+ <span className={cn("block truncate text-og-menu", props.active && "font-medium")}>
235
253
  {props.label}
236
254
  </span>
237
255
  {props.hint ? (
238
- <span className="mt-0.5 block truncate text-xs text-og-fg-subtle">{props.hint}</span>
256
+ <span className="mt-0.5 block truncate text-og-control text-og-fg-subtle">
257
+ {props.hint}
258
+ </span>
239
259
  ) : null}
240
260
  </span>
241
261
  {props.trailing ? <span className="ml-auto shrink-0">{props.trailing}</span> : null}
@@ -262,7 +282,7 @@ export function PickerBackHeader(props: {
262
282
  >
263
283
  <ChevronLeftIcon className="size-3.5 shrink-0 text-og-fg-subtle" />
264
284
  {props.icon}
265
- <span className="min-w-0 flex-1 truncate text-sm font-medium">{props.label}</span>
285
+ <span className="min-w-0 flex-1 truncate text-og-menu font-medium">{props.label}</span>
266
286
  </button>
267
287
  {props.trailing ? <div className="relative z-10 shrink-0">{props.trailing}</div> : null}
268
288
  </div>
@@ -352,9 +372,9 @@ export function ModelPolicyPickerMenu(
352
372
 
353
373
  let body: ReactNode;
354
374
  if (props.loading) {
355
- body = <p className="px-2 py-3 text-xs text-og-fg-subtle">{messages.loading}</p>;
375
+ body = <p className="px-2 py-3 text-og-control text-og-fg-subtle">{messages.loading}</p>;
356
376
  } else if (groups.length === 0) {
357
- body = <p className="px-2 py-3 text-xs text-og-fg-subtle">{messages.noModels}</p>;
377
+ body = <p className="px-2 py-3 text-og-control text-og-fg-subtle">{messages.noModels}</p>;
358
378
  } else if (effectiveNav.level === "providers") {
359
379
  body = (
360
380
  <div className="flex flex-col gap-0.5" data-testid="model-picker-providers">
@@ -449,11 +469,11 @@ export function ModelPolicyPickerMenu(
449
469
  ) : null
450
470
  }
451
471
  />
452
- <p className="px-2.5 pt-1 pb-1 text-xs font-medium tracking-wide text-og-fg-subtle uppercase">
472
+ <p className="px-2.5 pt-1 pb-1 text-og-control font-medium tracking-wide text-og-fg-subtle uppercase">
453
473
  {messages.thinking}
454
474
  </p>
455
475
  {focusModel.catalog.capabilities?.inputModalities.includes("image") === false ? (
456
- <p className="px-2.5 pb-1.5 text-xs leading-relaxed text-og-fg-subtle">
476
+ <p className="px-2.5 pb-1.5 text-og-control leading-relaxed text-og-fg-subtle">
457
477
  Unsupported attachments stay in the session but are hidden from this model.
458
478
  </p>
459
479
  ) : null}
@@ -467,7 +487,7 @@ export function ModelPolicyPickerMenu(
467
487
  disabled={!focusModel.selectable}
468
488
  onClick={() => selectEffort(focusModel, effort)}
469
489
  className={cn(
470
- "flex h-8 w-full cursor-pointer items-center rounded-og-sm px-2.5 text-left text-sm text-og-fg outline-none transition-colors hover:bg-og-surface-2 focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:cursor-not-allowed disabled:opacity-50",
490
+ "flex h-[var(--og-model-picker-effort-height)] w-full cursor-pointer items-center rounded-og-sm px-[var(--og-model-picker-row-padding-x)] text-left text-og-menu text-og-fg outline-none transition-colors hover:bg-og-surface-2 focus-visible:ring-2 focus-visible:ring-og-accent/40 disabled:cursor-not-allowed disabled:opacity-50",
471
491
  selected && "bg-og-surface-2",
472
492
  )}
473
493
  >
@@ -504,7 +524,7 @@ export function ModelPolicyPickerMenu(
504
524
  return (
505
525
  <div data-testid="model-picker-menu" className="relative overflow-hidden">
506
526
  {props.error ? (
507
- <p className="px-2 py-1 text-xs text-og-status-failed" role="alert">
527
+ <p className="px-2 py-1 text-og-control text-og-status-failed" role="alert">
508
528
  {props.error}
509
529
  </p>
510
530
  ) : null}
@@ -516,7 +536,14 @@ export function ModelPolicyPickerMenu(
516
536
  }
517
537
 
518
538
  export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
519
- const [open, setOpen] = useState(false);
539
+ const [uncontrolledOpen, setUncontrolledOpen] = useState(props.defaultOpen ?? false);
540
+ const open = props.open ?? uncontrolledOpen;
541
+ const setOpen = (next: boolean) => {
542
+ if (props.open === undefined) setUncontrolledOpen(next);
543
+ props.onOpenChange?.(next);
544
+ };
545
+ const triggerRef = useRef<HTMLButtonElement | null>(null);
546
+ const portalStyle = usePortalTokenStyle(triggerRef);
520
547
  const messages = useMemo(
521
548
  () => ({ ...defaultModelPolicyPickerMessages, ...props.messages }),
522
549
  [props.messages],
@@ -541,11 +568,12 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
541
568
  <DropdownMenu.Root open={open} onOpenChange={setOpen}>
542
569
  <DropdownMenu.Trigger asChild>
543
570
  <button
571
+ ref={triggerRef}
544
572
  type="button"
545
573
  disabled={props.disabled}
546
574
  aria-label={messages.label}
547
575
  className={cn(
548
- "inline-flex h-8 min-w-0 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-[7.5rem] max-sm:px-2",
576
+ "og-root inline-flex h-[var(--og-model-picker-trigger-height)] min-w-0 max-w-64 items-center gap-1 rounded-full border border-transparent px-2.5 text-og-control 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-[var(--og-model-picker-trigger-height-mobile)] max-sm:max-w-[7.5rem] max-sm:px-2",
549
577
  props.className,
550
578
  )}
551
579
  >
@@ -580,7 +608,12 @@ export function ModelPolicyPicker(props: ModelPolicyPickerProps) {
580
608
  sideOffset={8}
581
609
  collisionPadding={12}
582
610
  onCloseAutoFocus={(event) => event.preventDefault()}
583
- className="z-50 flex max-h-[min(20rem,var(--radix-dropdown-menu-content-available-height))] w-72 max-w-[calc(100vw-16px)] flex-col overflow-hidden rounded-og-lg border border-og-border bg-og-surface-1 p-1.5 text-og-fg shadow-og-lg"
611
+ className={cn(
612
+ "og-root z-50 flex max-h-[min(20rem,var(--radix-dropdown-menu-content-available-height))] w-[var(--og-model-picker-menu-width)] max-w-[calc(100vw-16px)] flex-col overflow-hidden rounded-og-lg border border-og-border bg-og-surface-1 p-[var(--og-model-picker-menu-padding)] text-og-fg shadow-og-lg",
613
+ props.contentClassName,
614
+ )}
615
+ style={{ ...portalStyle, ...props.contentStyle }}
616
+ data-testid="model-picker-content"
584
617
  >
585
618
  <div className="min-h-0 flex-1 overflow-y-auto overscroll-contain">
586
619
  <ModelPolicyPickerMenu {...props} nav={nav} onNavChange={setNav} />
@@ -40,6 +40,7 @@ import {
40
40
  import { DropdownMenu } from "radix-ui";
41
41
  import type { ComposerState } from "../hooks/use-composer";
42
42
  import type { QueueMutationKind, UseTurnQueueResult } from "../hooks/use-turn-queue";
43
+ import { type PortalTokenStyle, usePortalTokenStyle } from "../lib/use-portal-token-style";
43
44
  import { requestQueueDraftEdit } from "./queue-draft-policy";
44
45
  import { QueueErrorAlert, QueueStoppingStatus } from "./queue-surface-state";
45
46
 
@@ -80,6 +81,7 @@ export function QueueSurface({
80
81
  const [announcement, setAnnouncement] = useState("");
81
82
  const [draggedTurnId, setDraggedTurnId] = useState<string | null>(null);
82
83
  const surfaceRef = useRef<HTMLDivElement | null>(null);
84
+ const portalTokenStyle = usePortalTokenStyle(surfaceRef);
83
85
  const [keyboardDrag, setKeyboardDrag] = useState<{
84
86
  turnId: string;
85
87
  projectedIndex: number;
@@ -244,7 +246,7 @@ export function QueueSurface({
244
246
  return (
245
247
  <div
246
248
  ref={surfaceRef}
247
- className="mx-auto mb-2 w-full max-w-3xl shrink-0 px-4 sm:px-6"
249
+ className="og-root mx-auto mb-2 w-full max-w-3xl shrink-0 px-4 sm:px-6"
248
250
  data-testid="queue-surface"
249
251
  >
250
252
  <div className="overflow-hidden rounded-lg border border-border bg-surface/80 shadow-sm">
@@ -265,7 +267,7 @@ export function QueueSurface({
265
267
  aria-hidden="true"
266
268
  className={`size-3.5 shrink-0 text-fg-subtle transition-transform ${open ? "rotate-180" : ""}`}
267
269
  />
268
- <span className="text-xs font-medium text-fg">
270
+ <span className="text-og-control font-medium text-fg">
269
271
  {count > 0 ? `${count} queued prompt${count === 1 ? "" : "s"}` : null}
270
272
  {count > 0 && pendingInputCount > 0 ? " · " : null}
271
273
  {pendingInputCount > 0
@@ -273,7 +275,7 @@ export function QueueSurface({
273
275
  : null}
274
276
  </span>
275
277
  {readOnly ? (
276
- <span className="shrink-0 rounded border border-border px-1.5 py-0.5 text-2xs text-fg-subtle">
278
+ <span className="shrink-0 rounded border border-border px-1.5 py-0.5 text-og-xs text-fg-subtle">
277
279
  Read-only
278
280
  </span>
279
281
  ) : null}
@@ -282,7 +284,7 @@ export function QueueSurface({
282
284
  aria-hidden="true"
283
285
  className={`max-w-full truncate text-fg-muted ${
284
286
  collapsedPreview.collapsedVisual === collapsedPreview.summary
285
- ? "min-w-0 flex-1 text-xs"
287
+ ? "min-w-0 flex-1 text-og-control"
286
288
  : "shrink-0 text-[10px]"
287
289
  }`}
288
290
  data-testid="queue-collapsed-preview"
@@ -352,6 +354,7 @@ export function QueueSurface({
352
354
  pending={queue.mutationFor(turn.id)}
353
355
  confirmingReplace={replaceDraftFor === turn.id}
354
356
  keyboardDragging={keyboardDrag?.turnId === turn.id}
357
+ portalTokenStyle={portalTokenStyle}
355
358
  onHandleKeyDown={(event) => onHandleKeyDown(event, turn.id)}
356
359
  onMove={(nextIndex) => void moveToIndex(turn.id, nextIndex)}
357
360
  onEdit={canEditInComposer ? () => requestEdit(turn) : undefined}
@@ -393,7 +396,7 @@ export function QueueSurface({
393
396
  <DragOverlay modifiers={[verticalOnly]}>
394
397
  {draggedTurnId ? (
395
398
  <div
396
- className="max-h-20 w-[min(36rem,calc(100vw-2rem))] max-w-[calc(100vw-2rem)] overflow-hidden rounded-md border border-brand/40 bg-surface px-3 py-2 text-xs text-fg shadow-lg"
399
+ className="max-h-20 w-[min(36rem,calc(100vw-2rem))] max-w-[calc(100vw-2rem)] overflow-hidden rounded-md border border-brand/40 bg-surface px-3 py-2 text-og-control text-fg shadow-lg"
397
400
  data-testid="queue-drag-overlay"
398
401
  >
399
402
  <p
@@ -434,14 +437,18 @@ function PendingMachineInputs({
434
437
  }) {
435
438
  return (
436
439
  <section className="border-t border-border px-3 py-2">
437
- <p className="text-2xs font-medium text-fg-subtle">
440
+ <p className="text-og-xs font-medium text-fg-subtle">
438
441
  {attached
439
442
  ? `${inputs.length} update${inputs.length === 1 ? "" : "s"} will join this prompt`
440
443
  : "Incoming updates waiting to run"}
441
444
  </p>
442
445
  <ol className="mt-1 space-y-1">
443
446
  {inputs.map((input) => (
444
- <li key={input.id} className="line-clamp-3 break-words text-xs text-fg" dir="auto">
447
+ <li
448
+ key={input.id}
449
+ className="line-clamp-3 break-words text-og-control text-fg"
450
+ dir="auto"
451
+ >
445
452
  <strong className="capitalize text-fg-muted">
446
453
  {input.kind.replace("_terminal", "").replaceAll("_", " ")}
447
454
  </strong>{" "}
@@ -756,7 +763,7 @@ function QueuePrompt({
756
763
  <div className="w-full min-w-0 max-w-full">
757
764
  <div
758
765
  aria-label={`Queued prompt ${index + 1} summary: ${preview.summary}`}
759
- className="max-w-full overflow-hidden text-xs leading-5 text-fg"
766
+ className="max-w-full overflow-hidden text-og-control leading-5 text-fg"
760
767
  data-testid={`queue-prompt-preview-${index + 1}`}
761
768
  dir="auto"
762
769
  role="note"
@@ -771,7 +778,7 @@ function QueuePrompt({
771
778
  </span>
772
779
  {preview.visibleIdentity && preview.visibleIdentityLabel ? (
773
780
  <span
774
- className="flex min-w-0 max-w-[70%] shrink-0 items-center gap-1 text-2xs leading-4 text-fg-muted sm:mt-0.5 sm:max-w-full"
781
+ className="flex min-w-0 max-w-[70%] shrink-0 items-center gap-1 text-og-xs leading-4 text-fg-muted sm:mt-0.5 sm:max-w-full"
775
782
  data-testid={`queue-prompt-identity-row-${index + 1}`}
776
783
  >
777
784
  <span className="shrink-0 font-medium text-fg-subtle">
@@ -793,7 +800,7 @@ function QueuePrompt({
793
800
  aria-controls={fullContentId}
794
801
  aria-expanded={expanded}
795
802
  aria-label={`${expanded ? "Hide" : "Show"} full content for queued prompt ${index + 1}`}
796
- className="mt-1 inline-flex min-h-7 min-w-0 max-w-full items-center gap-1 whitespace-normal rounded-md text-left text-2xs font-medium text-fg outline-none focus-visible:ring-2 focus-visible:ring-ring/40 pointer-coarse:min-h-[44px]"
803
+ className="mt-1 inline-flex min-h-7 min-w-0 max-w-full items-center gap-1 whitespace-normal rounded-md text-left text-og-xs font-medium text-fg outline-none focus-visible:ring-2 focus-visible:ring-ring/40 pointer-coarse:min-h-[44px]"
797
804
  data-testid={`queue-prompt-disclosure-${index + 1}`}
798
805
  onClick={() => {
799
806
  const next = !expanded;
@@ -812,7 +819,7 @@ function QueuePrompt({
812
819
  id={fullContentId}
813
820
  role="region"
814
821
  aria-label={`Full content for queued prompt ${index + 1}`}
815
- className="mt-1.5 max-h-64 w-full min-w-0 max-w-full overflow-auto overscroll-contain rounded-md border border-border bg-surface-2/60 p-2 whitespace-pre-wrap break-all font-mono text-xs leading-5 text-fg [unicode-bidi:plaintext]"
822
+ className="mt-1.5 max-h-64 w-full min-w-0 max-w-full overflow-auto overscroll-contain rounded-md border border-border bg-surface-2/60 p-2 whitespace-pre-wrap break-all font-mono text-og-control leading-5 text-fg [unicode-bidi:plaintext]"
816
823
  data-testid={`queue-prompt-full-${index + 1}`}
817
824
  dir="auto"
818
825
  tabIndex={0}
@@ -837,13 +844,13 @@ function ReadOnlyQueueRow({
837
844
  }) {
838
845
  return (
839
846
  <li className="flex min-w-0 items-start gap-2 bg-surface px-3 py-2">
840
- <span className="mt-1 shrink-0 font-mono text-2xs text-fg-subtle">{index + 1}</span>
847
+ <span className="mt-1 shrink-0 font-mono text-og-xs text-fg-subtle">{index + 1}</span>
841
848
  <div className="min-w-0 flex-1">
842
849
  <QueuePrompt prompt={turn.prompt} index={index} onDisclosureChange={onDisclosureChange} />
843
850
  {attachedInputs.length > 0 ? (
844
851
  <PendingMachineInputs inputs={attachedInputs} attached />
845
852
  ) : null}
846
- <div className="mt-1 flex min-w-0 items-center gap-2 overflow-hidden whitespace-nowrap text-2xs text-fg-subtle">
853
+ <div className="mt-1 flex min-w-0 items-center gap-2 overflow-hidden whitespace-nowrap text-og-xs text-fg-subtle">
847
854
  {turn.resources.length > 0 ? (
848
855
  <span className="shrink-0">
849
856
  {turn.resources.length} resource{turn.resources.length === 1 ? "" : "s"}
@@ -870,6 +877,7 @@ function SortableQueueRow({
870
877
  pending,
871
878
  confirmingReplace,
872
879
  keyboardDragging,
880
+ portalTokenStyle,
873
881
  onHandleKeyDown,
874
882
  onMove,
875
883
  onEdit,
@@ -886,6 +894,7 @@ function SortableQueueRow({
886
894
  pending: QueueMutationKind | null;
887
895
  confirmingReplace: boolean;
888
896
  keyboardDragging: boolean;
897
+ portalTokenStyle: PortalTokenStyle;
889
898
  onHandleKeyDown: (event: ReactKeyboardEvent<HTMLButtonElement>) => void;
890
899
  onMove: (index: number) => void;
891
900
  onEdit?: (() => void) | undefined;
@@ -924,7 +933,7 @@ function SortableQueueRow({
924
933
  >
925
934
  <GripVerticalIcon className="size-3.5" />
926
935
  </button>
927
- <span className="col-start-2 row-start-1 mt-1 shrink-0 font-mono text-2xs text-fg-subtle">
936
+ <span className="col-start-2 row-start-1 mt-1 shrink-0 font-mono text-og-xs text-fg-subtle">
928
937
  {index + 1}
929
938
  </span>
930
939
  <div className="col-span-full row-start-2 min-w-0 sm:col-span-1 sm:col-start-3 sm:row-start-1">
@@ -932,7 +941,7 @@ function SortableQueueRow({
932
941
  {attachedInputs.length > 0 ? (
933
942
  <PendingMachineInputs inputs={attachedInputs} attached />
934
943
  ) : null}
935
- <div className="mt-1 flex min-w-0 items-center gap-2 overflow-hidden whitespace-nowrap text-2xs text-fg-subtle">
944
+ <div className="mt-1 flex min-w-0 items-center gap-2 overflow-hidden whitespace-nowrap text-og-xs text-fg-subtle">
936
945
  {turn.resources.length > 0 ? (
937
946
  <span className="shrink-0">
938
947
  {turn.resources.length} resource{turn.resources.length === 1 ? "" : "s"}
@@ -954,7 +963,7 @@ function SortableQueueRow({
954
963
  onClick={onSteer}
955
964
  aria-label={`Steer queued prompt ${index + 1}`}
956
965
  title="Steer — interrupt the current turn and send this message now"
957
- className="inline-flex h-7 shrink-0 items-center justify-center gap-1 rounded-md px-2 text-xs font-medium text-fg outline-none transition-[background-color] hover:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 pointer-coarse:min-h-[44px] pointer-coarse:min-w-[44px]"
966
+ className="inline-flex h-7 shrink-0 items-center justify-center gap-1 rounded-md px-2 text-og-control font-medium text-fg outline-none transition-[background-color] hover:bg-surface-2 focus-visible:ring-2 focus-visible:ring-ring/40 disabled:pointer-events-none disabled:opacity-50 pointer-coarse:min-h-[44px] pointer-coarse:min-w-[44px]"
958
967
  >
959
968
  {pending === "steer" ? (
960
969
  <Loader2Icon className="size-3.5 animate-spin" />
@@ -996,7 +1005,8 @@ function SortableQueueRow({
996
1005
  <DropdownMenu.Content
997
1006
  align="end"
998
1007
  sideOffset={4}
999
- className="z-50 w-48 max-w-[calc(100vw-16px)] rounded-md border border-border bg-surface p-1 text-xs text-fg shadow-lg"
1008
+ className="og-root z-50 w-48 max-w-[calc(100vw-16px)] rounded-md border border-border bg-surface p-1 text-og-control text-fg shadow-lg"
1009
+ style={portalTokenStyle}
1000
1010
  data-testid={`queue-actions-menu-${index + 1}`}
1001
1011
  >
1002
1012
  {onEdit ? (
@@ -1044,7 +1054,7 @@ function SortableQueueRow({
1044
1054
  </div>
1045
1055
  </div>
1046
1056
  {confirmingReplace ? (
1047
- <div className="mx-3 mb-2 rounded-md border border-status-waiting/30 bg-status-waiting/10 p-2 text-xs text-fg">
1057
+ <div className="mx-3 mb-2 rounded-md border border-status-waiting/30 bg-status-waiting/10 p-2 text-og-control text-fg">
1048
1058
  <p>Your composer already has a draft. Replace it with this queued prompt?</p>
1049
1059
  <p className="mt-0.5 text-fg-muted">
1050
1060
  The current draft will be permanently discarded; this queued prompt is preserved until
@@ -10,7 +10,7 @@ export function EmptyQueueStateSurface({ queue }: QueueStateProps) {
10
10
  const hasError = Boolean(queue.error || queue.mutationError);
11
11
  return (
12
12
  <div
13
- className="mx-auto mb-2 w-full max-w-3xl shrink-0 px-4 sm:px-6"
13
+ className="og-root mx-auto mb-2 w-full max-w-3xl shrink-0 px-4 sm:px-6"
14
14
  data-testid="queue-surface"
15
15
  >
16
16
  <div className="overflow-hidden rounded-lg border border-border bg-surface/80 shadow-sm">
@@ -31,7 +31,7 @@ export function QueueStoppingStatus({
31
31
  <div
32
32
  role="status"
33
33
  aria-live="polite"
34
- className={`flex min-h-11 items-center gap-2.5 bg-status-waiting/[0.07] px-3 py-2 text-xs text-fg ${
34
+ className={`flex min-h-11 items-center gap-2.5 bg-status-waiting/[0.07] px-3 py-2 text-og-control text-fg ${
35
35
  dividerAfter ? "border-b border-status-waiting/20" : ""
36
36
  }`}
37
37
  data-testid="stopping-previous-attempt"
@@ -64,7 +64,7 @@ export function QueueErrorAlert({
64
64
  <div className={`${dividerBefore ? "border-t border-border" : ""} p-2`}>
65
65
  <div
66
66
  role="alert"
67
- className="flex min-w-0 max-w-full flex-wrap items-start gap-2 rounded-md bg-status-failed/10 px-2 py-1.5 text-xs text-status-failed"
67
+ className="flex min-w-0 max-w-full flex-wrap items-start gap-2 rounded-md bg-status-failed/10 px-2 py-1.5 text-og-control text-status-failed"
68
68
  >
69
69
  <span
70
70
  role="region"
@@ -43,12 +43,12 @@ function QueueSurfaceFallback() {
43
43
  return (
44
44
  <div
45
45
  aria-live="polite"
46
- className="mx-auto mb-2 w-full max-w-3xl shrink-0 px-4 sm:px-6"
46
+ className="og-root mx-auto mb-2 w-full max-w-3xl shrink-0 px-4 sm:px-6"
47
47
  data-testid="queue-surface-loading"
48
48
  role="status"
49
49
  >
50
50
  <div className="overflow-hidden rounded-lg border border-border bg-surface/80 shadow-sm">
51
- <div className="flex items-center gap-2 px-3 py-2 text-xs text-fg-muted pointer-coarse:min-h-[44px]">
51
+ <div className="flex items-center gap-2 px-3 py-2 text-og-control text-fg-muted pointer-coarse:min-h-[44px]">
52
52
  <Loader2Icon
53
53
  aria-hidden="true"
54
54
  className="size-3.5 shrink-0 animate-spin motion-reduce:animate-none"
@@ -77,7 +77,7 @@ export function SessionStatus({ status, label, size = "md", className }: Session
77
77
  data-status={status}
78
78
  className={cn(
79
79
  "og-root inline-flex shrink-0 items-center rounded-full border font-medium",
80
- size === "sm" ? "gap-1 px-1.5 py-px text-og-xs" : "gap-1.5 px-2 py-0.5 text-xs",
80
+ size === "sm" ? "gap-1 px-1.5 py-px text-og-xs" : "gap-1.5 px-2 py-0.5 text-og-control",
81
81
  meta.badgeClassName,
82
82
  className,
83
83
  )}
package/src/lib/cn.ts CHANGED
@@ -15,7 +15,20 @@ import { extendTailwindMerge } from "tailwind-merge";
15
15
  const twMerge = extendTailwindMerge({
16
16
  extend: {
17
17
  classGroups: {
18
- "font-size": [{ text: ["og-xs", "og-sm", "og-base", "og-md"] }],
18
+ "font-size": [
19
+ {
20
+ text: [
21
+ "og-xs",
22
+ "og-sm",
23
+ "og-base",
24
+ "og-md",
25
+ "og-control",
26
+ "og-menu",
27
+ "og-composer",
28
+ "og-composer-wide",
29
+ ],
30
+ },
31
+ ],
19
32
  },
20
33
  },
21
34
  });
@@ -34,7 +34,7 @@ export function usePortalTokenStyle(sourceRef: RefObject<HTMLElement | null>): P
34
34
  const observer = new MutationObserver(sync);
35
35
  observer.observe(ancestor, {
36
36
  attributes: true,
37
- attributeFilter: ["class", "data-og-theme", "style"],
37
+ attributeFilter: ["class", "data-og-density", "data-og-theme", "style"],
38
38
  });
39
39
  observers.push(observer);
40
40
  ancestor = ancestor.parentElement;
package/src/provider.tsx CHANGED
@@ -152,17 +152,17 @@ function ApiContractMismatchScreen({ mismatch }: { mismatch: OpenGeniApiContract
152
152
  data-opengeni-api-contract-mismatch
153
153
  >
154
154
  <div className="w-full max-w-md rounded-xl border border-og-border bg-og-surface p-6 shadow-2xl">
155
- <p className="text-sm font-semibold text-og-fg">OpenGeni updated</p>
156
- <p className="mt-2 text-sm leading-6 text-og-muted">
155
+ <p className="text-og-menu font-semibold text-og-fg">OpenGeni updated</p>
156
+ <p className="mt-2 text-og-menu leading-6 text-og-muted">
157
157
  This tab cannot safely continue with the new server version. Reload it before sending or
158
158
  controlling work.
159
159
  </p>
160
- <p className="mt-3 font-mono text-xs text-og-subtle">
160
+ <p className="mt-3 font-mono text-og-control text-og-subtle">
161
161
  Client {mismatch.expected} · API {mismatch.actual}
162
162
  </p>
163
163
  <button
164
164
  type="button"
165
- className="mt-5 inline-flex h-9 items-center rounded-md bg-og-fg px-3 text-sm font-medium text-og-bg"
165
+ className="mt-5 inline-flex h-9 items-center rounded-md bg-og-fg px-3 text-og-menu font-medium text-og-bg"
166
166
  onClick={() => window.location.reload()}
167
167
  >
168
168
  Reload now
@@ -2,31 +2,69 @@
2
2
 
3
3
  import { ChevronRightIcon } from "lucide-react";
4
4
  import { DropdownMenu as DropdownMenuPrimitive } from "radix-ui";
5
- import type { ComponentProps } from "react";
5
+ import {
6
+ createContext,
7
+ useContext,
8
+ useRef,
9
+ type ComponentProps,
10
+ type Ref,
11
+ type RefObject,
12
+ } from "react";
6
13
  import { cn } from "../lib/cn";
14
+ import { usePortalTokenStyle } from "../lib/use-portal-token-style";
15
+
16
+ const DropdownMenuSourceContext = createContext<RefObject<HTMLElement | null> | null>(null);
17
+
18
+ function assignRef<T>(ref: Ref<T> | undefined, value: T | null) {
19
+ if (typeof ref === "function") ref(value);
20
+ else if (ref) ref.current = value;
21
+ }
7
22
 
8
23
  function DropdownMenu(props: ComponentProps<typeof DropdownMenuPrimitive.Root>) {
9
- return <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />;
24
+ const sourceRef = useRef<HTMLElement | null>(null);
25
+ return (
26
+ <DropdownMenuSourceContext.Provider value={sourceRef}>
27
+ <DropdownMenuPrimitive.Root data-slot="dropdown-menu" {...props} />
28
+ </DropdownMenuSourceContext.Provider>
29
+ );
10
30
  }
11
31
 
12
- function DropdownMenuTrigger(props: ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
13
- return <DropdownMenuPrimitive.Trigger data-slot="dropdown-menu-trigger" {...props} />;
32
+ function DropdownMenuTrigger({
33
+ ref,
34
+ ...props
35
+ }: ComponentProps<typeof DropdownMenuPrimitive.Trigger>) {
36
+ const sourceRef = useContext(DropdownMenuSourceContext);
37
+ return (
38
+ <DropdownMenuPrimitive.Trigger
39
+ data-slot="dropdown-menu-trigger"
40
+ ref={(node) => {
41
+ if (sourceRef) sourceRef.current = node;
42
+ assignRef(ref, node);
43
+ }}
44
+ {...props}
45
+ />
46
+ );
14
47
  }
15
48
 
16
49
  function DropdownMenuContent({
17
50
  className,
18
51
  sideOffset = 4,
52
+ style,
19
53
  ...props
20
54
  }: ComponentProps<typeof DropdownMenuPrimitive.Content>) {
55
+ const sourceRef = useContext(DropdownMenuSourceContext);
56
+ const fallbackRef = useRef<HTMLElement | null>(null);
57
+ const portalStyle = usePortalTokenStyle(sourceRef ?? fallbackRef);
21
58
  return (
22
59
  <DropdownMenuPrimitive.Portal>
23
60
  <DropdownMenuPrimitive.Content
24
61
  data-slot="dropdown-menu-content"
25
62
  sideOffset={sideOffset}
26
63
  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",
64
+ "og-root 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
65
  className,
29
66
  )}
67
+ style={{ ...portalStyle, ...style }}
30
68
  {...props}
31
69
  />
32
70
  </DropdownMenuPrimitive.Portal>
@@ -48,7 +86,7 @@ function DropdownMenuItem({
48
86
  data-inset={inset}
49
87
  data-variant={variant}
50
88
  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!",
89
+ "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-og-menu 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
90
  className,
53
91
  )}
54
92
  {...props}
@@ -84,7 +122,7 @@ function DropdownMenuSubTrigger({
84
122
  data-slot="dropdown-menu-sub-trigger"
85
123
  data-inset={inset}
86
124
  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",
125
+ "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-og-menu 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
126
  className,
89
127
  )}
90
128
  {...props}
@@ -97,15 +135,20 @@ function DropdownMenuSubTrigger({
97
135
 
98
136
  function DropdownMenuSubContent({
99
137
  className,
138
+ style,
100
139
  ...props
101
140
  }: ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
141
+ const sourceRef = useContext(DropdownMenuSourceContext);
142
+ const fallbackRef = useRef<HTMLElement | null>(null);
143
+ const portalStyle = usePortalTokenStyle(sourceRef ?? fallbackRef);
102
144
  return (
103
145
  <DropdownMenuPrimitive.SubContent
104
146
  data-slot="dropdown-menu-sub-content"
105
147
  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",
148
+ "og-root 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
149
  className,
108
150
  )}
151
+ style={{ ...portalStyle, ...style }}
109
152
  {...props}
110
153
  />
111
154
  );
@@ -866,8 +866,8 @@ export function RealtimeVoiceModelPanel(props: {
866
866
  <div className="flex shrink-0 items-start gap-1 px-2 pt-1 pb-1.5">
867
867
  {props.leading}
868
868
  <div className="min-w-0">
869
- <div className="text-sm font-medium text-og-fg">Voice model</div>
870
- <p className="mt-0.5 text-xs text-og-fg-subtle">
869
+ <div className="text-og-menu font-medium text-og-fg">Voice model</div>
870
+ <p className="mt-0.5 text-og-control text-og-fg-subtle">
871
871
  Used when you start a voice conversation.
872
872
  </p>
873
873
  </div>