@flamingo-stack/openframe-frontend-core 0.0.489 → 0.0.490-snapshot.20260728181420
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/{chunk-IN4IO7BL.cjs → chunk-2RTNC3KP.cjs} +1212 -970
- package/dist/chunk-2RTNC3KP.cjs.map +1 -0
- package/dist/{chunk-5VSEMWWE.js → chunk-2YQK6LHR.js} +5 -5
- package/dist/{chunk-W6ZUOWSB.js → chunk-35CTCUJ7.js} +6 -6
- package/dist/{chunk-ZIAS5NNL.cjs → chunk-6O3YTVLG.cjs} +12 -12
- package/dist/{chunk-ZIAS5NNL.cjs.map → chunk-6O3YTVLG.cjs.map} +1 -1
- package/dist/{chunk-RW2LPL72.js → chunk-6WTCJI5R.js} +2 -2
- package/dist/{chunk-X2YF5TJS.js → chunk-6XNWT2KC.js} +3 -3
- package/dist/{chunk-VLOE7MI2.cjs → chunk-7NRRONK5.cjs} +32 -32
- package/dist/{chunk-VLOE7MI2.cjs.map → chunk-7NRRONK5.cjs.map} +1 -1
- package/dist/{chunk-YMJZOANX.js → chunk-CYIUPI7J.js} +6298 -6056
- package/dist/chunk-CYIUPI7J.js.map +1 -0
- package/dist/{chunk-FKPXFTZT.js → chunk-CZTKQVIQ.js} +3 -3
- package/dist/{chunk-5YMY4BAW.cjs → chunk-EHHX22JL.cjs} +93 -93
- package/dist/{chunk-5YMY4BAW.cjs.map → chunk-EHHX22JL.cjs.map} +1 -1
- package/dist/{chunk-LLEDO4P7.cjs → chunk-GL33BHGZ.cjs} +73 -73
- package/dist/{chunk-LLEDO4P7.cjs.map → chunk-GL33BHGZ.cjs.map} +1 -1
- package/dist/{chunk-N4MTNTSX.js → chunk-GTMW4KY7.js} +6 -6
- package/dist/{chunk-H3YRJ3LZ.cjs → chunk-HRMOCJKU.cjs} +12 -12
- package/dist/{chunk-H3YRJ3LZ.cjs.map → chunk-HRMOCJKU.cjs.map} +1 -1
- package/dist/{chunk-3OIB763B.cjs → chunk-ITV6LSG7.cjs} +19 -19
- package/dist/{chunk-3OIB763B.cjs.map → chunk-ITV6LSG7.cjs.map} +1 -1
- package/dist/{chunk-EU6QNO4S.js → chunk-IUGJAILN.js} +3 -3
- package/dist/{chunk-J7H32E3R.cjs → chunk-J2V4B6FF.cjs} +18 -18
- package/dist/{chunk-J7H32E3R.cjs.map → chunk-J2V4B6FF.cjs.map} +1 -1
- package/dist/{chunk-RO7Q56ZK.cjs → chunk-PVSXTCJY.cjs} +40 -40
- package/dist/{chunk-RO7Q56ZK.cjs.map → chunk-PVSXTCJY.cjs.map} +1 -1
- package/dist/{chunk-B4YR342L.js → chunk-QF54RPHZ.js} +6 -6
- package/dist/{chunk-GZK7FWJJ.js → chunk-QKWOBV55.js} +12 -12
- package/dist/chunk-QKWOBV55.js.map +1 -0
- package/dist/{chunk-Z7Y63IFU.cjs → chunk-QNFAZIFC.cjs} +5 -5
- package/dist/{chunk-Z7Y63IFU.cjs.map → chunk-QNFAZIFC.cjs.map} +1 -1
- package/dist/{chunk-STYA77PH.cjs → chunk-SGL6URIJ.cjs} +12 -12
- package/dist/chunk-SGL6URIJ.cjs.map +1 -0
- package/dist/{chunk-TWGYBXYV.js → chunk-T4BUS6JV.js} +3 -3
- package/dist/{chunk-KP5KRRD6.cjs → chunk-THC3K262.cjs} +3 -3
- package/dist/{chunk-KP5KRRD6.cjs.map → chunk-THC3K262.cjs.map} +1 -1
- package/dist/{chunk-BIKOJ4FH.cjs → chunk-TTYJAZ7P.cjs} +58 -32
- package/dist/chunk-TTYJAZ7P.cjs.map +1 -0
- package/dist/{chunk-BGWDDB7Y.js → chunk-U2JIGUXF.js} +4 -4
- package/dist/{chunk-2RNJYJWO.cjs → chunk-VD4QDSP7.cjs} +40 -40
- package/dist/{chunk-2RNJYJWO.cjs.map → chunk-VD4QDSP7.cjs.map} +1 -1
- package/dist/{chunk-PB3BTZDU.js → chunk-WEUNPE3S.js} +139 -113
- package/dist/chunk-WEUNPE3S.js.map +1 -0
- package/dist/{chunk-2IHN7NNB.cjs → chunk-YKE2KZ2Y.cjs} +33 -33
- package/dist/{chunk-2IHN7NNB.cjs.map → chunk-YKE2KZ2Y.cjs.map} +1 -1
- package/dist/{chunk-VL4LUO32.js → chunk-ZDUJ6BDI.js} +4 -4
- package/dist/{chunk-K7MNWYYI.js → chunk-ZEC5U5T7.js} +2 -2
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/chat-message-enhanced.d.ts.map +1 -1
- package/dist/components/chat/guide-display.d.ts +14 -0
- package/dist/components/chat/guide-display.d.ts.map +1 -0
- package/dist/components/chat/hooks/use-nats-chat-adapter.d.ts.map +1 -1
- package/dist/components/chat/hooks/use-realtime-chunk-processor.d.ts.map +1 -1
- package/dist/components/chat/index.cjs +6 -4
- package/dist/components/chat/index.cjs.map +1 -1
- package/dist/components/chat/index.d.ts +1 -0
- package/dist/components/chat/index.d.ts.map +1 -1
- package/dist/components/chat/index.js +5 -3
- package/dist/components/chat/types/component.types.d.ts +6 -0
- package/dist/components/chat/types/component.types.d.ts.map +1 -1
- package/dist/components/chat/types/message.types.d.ts +15 -2
- package/dist/components/chat/types/message.types.d.ts.map +1 -1
- package/dist/components/chat/types/processing.types.d.ts +3 -0
- package/dist/components/chat/types/processing.types.d.ts.map +1 -1
- package/dist/components/chat/utils/chunk-parser.d.ts.map +1 -1
- package/dist/components/chat/utils/message-segment-accumulator.d.ts +5 -0
- package/dist/components/chat/utils/message-segment-accumulator.d.ts.map +1 -1
- package/dist/components/chat/utils/process-historical-messages.d.ts.map +1 -1
- package/dist/components/contact/index.cjs +5 -5
- package/dist/components/contact/index.js +4 -4
- package/dist/components/docs/index.cjs +7 -7
- package/dist/components/docs/index.js +6 -6
- package/dist/components/embeds/index.cjs +5 -5
- package/dist/components/embeds/index.js +4 -4
- package/dist/components/faq/index.cjs +6 -6
- package/dist/components/faq/index.js +5 -5
- package/dist/components/features/index.cjs +4 -4
- package/dist/components/features/index.js +3 -3
- package/dist/components/features/select-button.d.ts.map +1 -1
- package/dist/components/help-center-pages/index.cjs +23 -23
- package/dist/components/help-center-pages/index.js +14 -14
- package/dist/components/index.cjs +163 -161
- package/dist/components/index.cjs.map +1 -1
- package/dist/components/index.js +14 -12
- package/dist/components/index.js.map +1 -1
- package/dist/components/layout/page-layout.d.ts +7 -1
- package/dist/components/layout/page-layout.d.ts.map +1 -1
- package/dist/components/layout/title-block.d.ts +23 -1
- package/dist/components/layout/title-block.d.ts.map +1 -1
- package/dist/components/navigation/index.cjs +4 -4
- package/dist/components/navigation/index.js +3 -3
- package/dist/components/onboarding-guides/index.cjs +8 -8
- package/dist/components/onboarding-guides/index.js +7 -7
- package/dist/components/related-content/index.cjs +6 -6
- package/dist/components/related-content/index.js +5 -5
- package/dist/components/tickets/index.cjs +7 -7
- package/dist/components/tickets/index.js +6 -6
- package/dist/components/ui/autocomplete.d.ts.map +1 -1
- package/dist/components/ui/checkbox-block.d.ts.map +1 -1
- package/dist/components/ui/date-picker.d.ts.map +1 -1
- package/dist/components/ui/field-wrapper.d.ts +10 -1
- package/dist/components/ui/field-wrapper.d.ts.map +1 -1
- package/dist/components/ui/file-manager/index.cjs +50 -50
- package/dist/components/ui/file-manager/index.js +1 -1
- package/dist/components/ui/floating-tooltip.d.ts +8 -1
- package/dist/components/ui/floating-tooltip.d.ts.map +1 -1
- package/dist/components/ui/index.cjs +6 -4
- package/dist/components/ui/index.cjs.map +1 -1
- package/dist/components/ui/index.js +5 -3
- package/dist/components/ui/input-trigger.d.ts.map +1 -1
- package/dist/components/ui/input.d.ts.map +1 -1
- package/dist/components/ui/page-actions.d.ts +7 -1
- package/dist/components/ui/page-actions.d.ts.map +1 -1
- package/dist/components/ui/radio-group.d.ts.map +1 -1
- package/dist/components/ui/search-input.d.ts.map +1 -1
- package/dist/components/ui/select.d.ts.map +1 -1
- package/dist/components/ui/tab-navigation.d.ts +10 -1
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/components/ui/tag-search-input.d.ts.map +1 -1
- package/dist/components/ui/textarea.d.ts.map +1 -1
- package/dist/components/ui/truncate-text.d.ts +2 -2
- package/dist/components/ui/truncate-text.d.ts.map +1 -1
- package/dist/hooks/index.cjs +4 -2
- package/dist/hooks/index.cjs.map +1 -1
- package/dist/hooks/index.js +3 -1
- package/dist/hooks/ui/index.d.ts +1 -0
- package/dist/hooks/ui/index.d.ts.map +1 -1
- package/dist/hooks/ui/use-is-truncated.d.ts +33 -0
- package/dist/hooks/ui/use-is-truncated.d.ts.map +1 -0
- package/dist/index.cjs +8 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +7 -3
- package/dist/utils/index.cjs.map +1 -1
- package/dist/utils/index.js.map +1 -1
- package/package.json +1 -1
- package/src/components/chat/chat-message-enhanced.tsx +63 -52
- package/src/components/chat/guide-display.tsx +44 -0
- package/src/components/chat/hooks/use-nats-chat-adapter.ts +2 -0
- package/src/components/chat/hooks/use-realtime-chunk-processor.ts +10 -0
- package/src/components/chat/index.ts +1 -0
- package/src/components/chat/types/component.types.ts +9 -0
- package/src/components/chat/types/message.types.ts +17 -1
- package/src/components/chat/types/processing.types.ts +1 -0
- package/src/components/chat/utils/chunk-parser.ts +6 -0
- package/src/components/chat/utils/message-segment-accumulator.ts +19 -0
- package/src/components/chat/utils/process-historical-messages.ts +6 -0
- package/src/components/features/select-button.tsx +4 -1
- package/src/components/layout/page-layout.tsx +16 -0
- package/src/components/layout/title-block.tsx +106 -7
- package/src/components/ui/.field-wrapper.md +5 -4
- package/src/components/ui/.tab-navigation.md +14 -1
- package/src/components/ui/autocomplete.tsx +13 -1
- package/src/components/ui/button/button.tsx +1 -1
- package/src/components/ui/checkbox-block.tsx +27 -3
- package/src/components/ui/date-picker.tsx +15 -1
- package/src/components/ui/field-wrapper.tsx +12 -6
- package/src/components/ui/floating-tooltip.tsx +9 -1
- package/src/components/ui/input-trigger.tsx +8 -1
- package/src/components/ui/input.tsx +8 -1
- package/src/components/ui/page-actions.tsx +142 -30
- package/src/components/ui/radio-group.tsx +28 -3
- package/src/components/ui/search-input.tsx +2 -1
- package/src/components/ui/select.tsx +7 -0
- package/src/components/ui/tab-navigation.tsx +26 -4
- package/src/components/ui/tag-search-input.tsx +9 -2
- package/src/components/ui/textarea.tsx +8 -1
- package/src/components/ui/truncate-text.tsx +3 -18
- package/src/hooks/ui/index.ts +1 -0
- package/src/hooks/ui/use-is-truncated.ts +66 -0
- package/src/stories/ChatMessageEnhanced.stories.tsx +48 -1
- package/dist/chunk-BIKOJ4FH.cjs.map +0 -1
- package/dist/chunk-GZK7FWJJ.js.map +0 -1
- package/dist/chunk-IN4IO7BL.cjs.map +0 -1
- package/dist/chunk-PB3BTZDU.js.map +0 -1
- package/dist/chunk-STYA77PH.cjs.map +0 -1
- package/dist/chunk-YMJZOANX.js.map +0 -1
- /package/dist/{chunk-5VSEMWWE.js.map → chunk-2YQK6LHR.js.map} +0 -0
- /package/dist/{chunk-W6ZUOWSB.js.map → chunk-35CTCUJ7.js.map} +0 -0
- /package/dist/{chunk-RW2LPL72.js.map → chunk-6WTCJI5R.js.map} +0 -0
- /package/dist/{chunk-X2YF5TJS.js.map → chunk-6XNWT2KC.js.map} +0 -0
- /package/dist/{chunk-FKPXFTZT.js.map → chunk-CZTKQVIQ.js.map} +0 -0
- /package/dist/{chunk-N4MTNTSX.js.map → chunk-GTMW4KY7.js.map} +0 -0
- /package/dist/{chunk-EU6QNO4S.js.map → chunk-IUGJAILN.js.map} +0 -0
- /package/dist/{chunk-B4YR342L.js.map → chunk-QF54RPHZ.js.map} +0 -0
- /package/dist/{chunk-TWGYBXYV.js.map → chunk-T4BUS6JV.js.map} +0 -0
- /package/dist/{chunk-BGWDDB7Y.js.map → chunk-U2JIGUXF.js.map} +0 -0
- /package/dist/{chunk-VL4LUO32.js.map → chunk-ZDUJ6BDI.js.map} +0 -0
- /package/dist/{chunk-K7MNWYYI.js.map → chunk-ZEC5U5T7.js.map} +0 -0
package/package.json
CHANGED
|
@@ -11,6 +11,7 @@ import { ApprovalBatchMessage } from "./approval-batch-message"
|
|
|
11
11
|
import { ErrorMessageDisplay } from "./error-message-display"
|
|
12
12
|
import { ContextCompactionDisplay } from "./context-compaction-display"
|
|
13
13
|
import { ThinkingDisplay } from "./thinking-display"
|
|
14
|
+
import { GuideDisplay } from "./guide-display"
|
|
14
15
|
import { SimpleMarkdownRenderer } from "../ui/simple-markdown-renderer"
|
|
15
16
|
import type { ChatRef } from "./chat-ref.types"
|
|
16
17
|
import { remarkCardLinks } from "./remark-card-links"
|
|
@@ -172,7 +173,12 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
172
173
|
// duplicates so two siblings never collide on the same React key.
|
|
173
174
|
const emittedBlockKeys = new Set<string>()
|
|
174
175
|
segments.forEach((segment, segIdx) => {
|
|
175
|
-
|
|
176
|
+
// Both markdown-bearing segment types take part: a `guide` body is
|
|
177
|
+
// authored by the same LLM and carries the same `[card://]` markers,
|
|
178
|
+
// so skipping it here left its markers with no `inlineByKey` entry and
|
|
179
|
+
// no hoisted card — the `<a card://>` override silently degraded them
|
|
180
|
+
// to a bare title / id.
|
|
181
|
+
if (segment.type !== 'text' && segment.type !== 'guide') return
|
|
176
182
|
const text = segment.text
|
|
177
183
|
const parts: SegmentPart[] = []
|
|
178
184
|
let cursor = 0
|
|
@@ -384,6 +390,52 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
384
390
|
}
|
|
385
391
|
}, [hasMarkerSupport, hasMentionSupport, renderMention, chatRefs, renderingPlan, NavLinkAnchor])
|
|
386
392
|
|
|
393
|
+
/**
|
|
394
|
+
* Body of a markdown-bearing segment (`text` / `guide`).
|
|
395
|
+
*
|
|
396
|
+
* No block markers in this segment → a single SimpleMarkdownRenderer call
|
|
397
|
+
* (the vast majority of messages). Otherwise the text is split at each
|
|
398
|
+
* marker and block payloads are interleaved: each text chunk includes its
|
|
399
|
+
* trailing marker so the inline pill renders at the right position via the
|
|
400
|
+
* `<a>` override, and block payloads land AS SIBLINGS between text chunks —
|
|
401
|
+
* HTML-valid (block DOM never nests inside `<p>`) AND positionally correct
|
|
402
|
+
* (the block appears where the marker is in the flow, not at the segment's
|
|
403
|
+
* end). Stable React keys come from the card key (block) and chunk position
|
|
404
|
+
* (text); streaming token-by-token reuses the same React instances so
|
|
405
|
+
* `<Video>` doesn't remount mid-play.
|
|
406
|
+
*/
|
|
407
|
+
const renderSegmentBody = (segIndex: number, text: string) => {
|
|
408
|
+
const parts = renderingPlan?.partsBySegment.get(segIndex)
|
|
409
|
+
if (!parts || parts.length === 0) {
|
|
410
|
+
return (
|
|
411
|
+
<SimpleMarkdownRenderer
|
|
412
|
+
content={text}
|
|
413
|
+
textSize="compact"
|
|
414
|
+
additionalRemarkPlugins={cardRemarkPlugins}
|
|
415
|
+
componentOverrides={cardComponentOverrides}
|
|
416
|
+
/>
|
|
417
|
+
)
|
|
418
|
+
}
|
|
419
|
+
return parts.map((part, pIdx) => {
|
|
420
|
+
if (part.kind === 'text') {
|
|
421
|
+
return (
|
|
422
|
+
<SimpleMarkdownRenderer
|
|
423
|
+
key={`t-${pIdx}`}
|
|
424
|
+
content={part.text}
|
|
425
|
+
textSize="compact"
|
|
426
|
+
additionalRemarkPlugins={cardRemarkPlugins}
|
|
427
|
+
componentOverrides={cardComponentOverrides}
|
|
428
|
+
/>
|
|
429
|
+
)
|
|
430
|
+
}
|
|
431
|
+
return (
|
|
432
|
+
<div key={`b-${part.key}`} className="my-3">
|
|
433
|
+
{part.node}
|
|
434
|
+
</div>
|
|
435
|
+
)
|
|
436
|
+
})
|
|
437
|
+
}
|
|
438
|
+
|
|
387
439
|
const getAvatarProps = () => {
|
|
388
440
|
const displayName = name || (isUser ? "User" : assistantType === 'mingo' ? "Mingo" : "Fae")
|
|
389
441
|
const isMingo = assistantType === 'mingo'
|
|
@@ -488,60 +540,19 @@ const ChatMessageEnhanced = forwardRef<HTMLDivElement, ChatMessageEnhancedProps>
|
|
|
488
540
|
{(!isSystem || segments.length > 0) && <div className="flex flex-col gap-2">
|
|
489
541
|
{segments.map((segment, index) => {
|
|
490
542
|
if (segment.type === 'text') {
|
|
491
|
-
const parts = renderingPlan?.partsBySegment.get(index)
|
|
492
|
-
const wrapperClass = cn(
|
|
493
|
-
"min-w-0 w-full break-words text-h4",
|
|
494
|
-
isError ? "text-ods-error" : "text-ods-text-primary",
|
|
495
|
-
)
|
|
496
|
-
// No block markers in this segment → single
|
|
497
|
-
// SimpleMarkdownRenderer call (existing behaviour
|
|
498
|
-
// preserved for the vast majority of messages).
|
|
499
|
-
if (!parts || parts.length === 0) {
|
|
500
|
-
return (
|
|
501
|
-
<div key={index} className={wrapperClass}>
|
|
502
|
-
<SimpleMarkdownRenderer
|
|
503
|
-
content={segment.text}
|
|
504
|
-
textSize="compact"
|
|
505
|
-
additionalRemarkPlugins={cardRemarkPlugins}
|
|
506
|
-
componentOverrides={cardComponentOverrides}
|
|
507
|
-
/>
|
|
508
|
-
</div>
|
|
509
|
-
)
|
|
510
|
-
}
|
|
511
|
-
// Block markers present → split text at each marker
|
|
512
|
-
// and interleave block payloads. Each text chunk
|
|
513
|
-
// includes its trailing marker so the inline pill
|
|
514
|
-
// renders at the right position via the `<a>`
|
|
515
|
-
// override. Block payloads land AS SIBLINGS between
|
|
516
|
-
// text chunks — HTML-valid (block DOM never nests
|
|
517
|
-
// inside `<p>`) AND positionally correct (block
|
|
518
|
-
// appears where the marker is in the flow, not at
|
|
519
|
-
// the segment's end). Stable React keys come from
|
|
520
|
-
// the card key (block) and chunk position (text);
|
|
521
|
-
// streaming token-by-token reuses the same React
|
|
522
|
-
// instances so `<Video>` doesn't remount mid-play.
|
|
523
543
|
return (
|
|
524
|
-
<div
|
|
525
|
-
{
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
additionalRemarkPlugins={cardRemarkPlugins}
|
|
533
|
-
componentOverrides={cardComponentOverrides}
|
|
534
|
-
/>
|
|
535
|
-
)
|
|
536
|
-
}
|
|
537
|
-
return (
|
|
538
|
-
<div key={`b-${part.key}`} className="my-3">
|
|
539
|
-
{part.node}
|
|
540
|
-
</div>
|
|
541
|
-
)
|
|
542
|
-
})}
|
|
544
|
+
<div
|
|
545
|
+
key={index}
|
|
546
|
+
className={cn(
|
|
547
|
+
"min-w-0 w-full break-words text-h4",
|
|
548
|
+
isError ? "text-ods-error" : "text-ods-text-primary",
|
|
549
|
+
)}
|
|
550
|
+
>
|
|
551
|
+
{renderSegmentBody(index, segment.text)}
|
|
543
552
|
</div>
|
|
544
553
|
)
|
|
554
|
+
} else if (segment.type === 'guide') {
|
|
555
|
+
return <GuideDisplay key={index}>{renderSegmentBody(index, segment.text)}</GuideDisplay>
|
|
545
556
|
} else if (segment.type === 'tool_execution') {
|
|
546
557
|
return (
|
|
547
558
|
<ToolExecutionDisplay
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { forwardRef } from "react"
|
|
4
|
+
|
|
5
|
+
import { cn } from "../../utils/cn"
|
|
6
|
+
import { CompassIcon } from "../icons-v2-generated"
|
|
7
|
+
import type { GuideDisplayProps } from "./types"
|
|
8
|
+
|
|
9
|
+
const GUIDE_LABEL = "OpenFrame Guide"
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* `GUIDE` segment — the assistant's how-to/documentation answer, framed as a
|
|
13
|
+
* titled card instead of a bare paragraph.
|
|
14
|
+
*
|
|
15
|
+
* Chrome only: the caller renders the markdown body and passes it as children,
|
|
16
|
+
* so a guide goes through the SAME body pipeline as a text segment
|
|
17
|
+
* (`[card://]` splitting + hoisted entity cards + mention chips). Rendering the
|
|
18
|
+
* raw markdown here instead would bypass that plan and degrade every card
|
|
19
|
+
* marker inside a guide to a bare title.
|
|
20
|
+
*/
|
|
21
|
+
const GuideDisplay = forwardRef<HTMLDivElement, GuideDisplayProps>(
|
|
22
|
+
({ className, children, ...props }, ref) => {
|
|
23
|
+
return (
|
|
24
|
+
<div
|
|
25
|
+
ref={ref}
|
|
26
|
+
className={cn(
|
|
27
|
+
"flex flex-col gap-[var(--spacing-system-m)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-m)]",
|
|
28
|
+
className
|
|
29
|
+
)}
|
|
30
|
+
{...props}
|
|
31
|
+
>
|
|
32
|
+
<div className="flex items-center gap-[var(--spacing-system-xs)]">
|
|
33
|
+
<CompassIcon size={16} className="shrink-0 text-ods-open-yellow" />
|
|
34
|
+
<span className="text-h5 text-ods-open-yellow">{GUIDE_LABEL}</span>
|
|
35
|
+
</div>
|
|
36
|
+
<div className="min-w-0 w-full break-words text-h4 text-ods-text-primary">{children}</div>
|
|
37
|
+
</div>
|
|
38
|
+
)
|
|
39
|
+
}
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
GuideDisplay.displayName = "GuideDisplay"
|
|
43
|
+
|
|
44
|
+
export { GuideDisplay }
|
|
@@ -442,6 +442,8 @@ export function appendToTrailingAssistant(
|
|
|
442
442
|
merged[merged.length - 1] = { type: 'text', text: tail.text + seg.text }
|
|
443
443
|
} else if (seg.type === 'thinking' && tail?.type === 'thinking') {
|
|
444
444
|
merged[merged.length - 1] = { type: 'thinking', text: tail.text + seg.text }
|
|
445
|
+
} else if (seg.type === 'guide' && tail?.type === 'guide') {
|
|
446
|
+
merged[merged.length - 1] = { type: 'guide', text: tail.text + seg.text }
|
|
445
447
|
} else if (seg.type === 'approval_batch') {
|
|
446
448
|
// Approval deltas must be IDEMPOTENT: the escalated-result emit can be
|
|
447
449
|
// seen twice (live + catch-up replay over hydrated history), so upsert
|
|
@@ -173,6 +173,16 @@ export function useRealtimeChunkProcessor(
|
|
|
173
173
|
break
|
|
174
174
|
}
|
|
175
175
|
|
|
176
|
+
case 'guide': {
|
|
177
|
+
const segments = accumulator.appendGuide(action.text)
|
|
178
|
+
if (isInStreamRef.current || !hasEverStreamedRef.current) {
|
|
179
|
+
emitSegments(segments)
|
|
180
|
+
} else {
|
|
181
|
+
emitSegments([{ type: 'guide', text: action.text }], { append: true })
|
|
182
|
+
}
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
|
|
176
186
|
case 'tool_execution': {
|
|
177
187
|
// A starting tool run means the agent's turn is in progress even
|
|
178
188
|
// when this lands after MESSAGE_END (approved commands execute
|
|
@@ -6,6 +6,7 @@ export * from './approval-batch-message'
|
|
|
6
6
|
export * from './context-compaction-display'
|
|
7
7
|
export * from './expand-chevron'
|
|
8
8
|
export * from './thinking-display'
|
|
9
|
+
export * from './guide-display'
|
|
9
10
|
export * from './error-message-display'
|
|
10
11
|
export * from './chat-container'
|
|
11
12
|
export * from './chat-input'
|
|
@@ -556,6 +556,15 @@ export interface ThinkingDisplayProps extends HTMLAttributes<HTMLDivElement> {
|
|
|
556
556
|
isStreaming?: boolean
|
|
557
557
|
}
|
|
558
558
|
|
|
559
|
+
// ========== Guide Display Props ==========
|
|
560
|
+
|
|
561
|
+
export interface GuideDisplayProps extends HTMLAttributes<HTMLDivElement> {
|
|
562
|
+
/** Rendered guide body. `ChatMessageEnhanced` passes the SAME markdown
|
|
563
|
+
* output a text segment gets, so `[card://]` cards and mention chips
|
|
564
|
+
* behave identically inside a guide. */
|
|
565
|
+
children?: ReactNode
|
|
566
|
+
}
|
|
567
|
+
|
|
559
568
|
// ========== Model Display Props ==========
|
|
560
569
|
|
|
561
570
|
export interface ModelDisplayProps extends HTMLAttributes<HTMLDivElement> {
|
|
@@ -10,6 +10,7 @@ import type { AssistantType, AuthorType, ChatApprovalStatus, MessageOwner } from
|
|
|
10
10
|
export const MESSAGE_TYPE = {
|
|
11
11
|
TEXT: 'TEXT',
|
|
12
12
|
THINKING: 'THINKING',
|
|
13
|
+
GUIDE: 'GUIDE',
|
|
13
14
|
EXECUTING_TOOL: 'EXECUTING_TOOL',
|
|
14
15
|
EXECUTED_TOOL: 'EXECUTED_TOOL',
|
|
15
16
|
APPROVAL_REQUEST: 'APPROVAL_REQUEST',
|
|
@@ -170,6 +171,15 @@ export type ThinkingSegment = {
|
|
|
170
171
|
text: string
|
|
171
172
|
}
|
|
172
173
|
|
|
174
|
+
/** Guide answer body — the assistant's how-to/documentation reply, rendered as
|
|
175
|
+
* a titled "OpenFrame Guide" card instead of a bare paragraph. `text` is
|
|
176
|
+
* markdown, streamed in fragments like a `text` segment and coalesced by the
|
|
177
|
+
* accumulator. */
|
|
178
|
+
export type GuideSegment = {
|
|
179
|
+
type: 'guide'
|
|
180
|
+
text: string
|
|
181
|
+
}
|
|
182
|
+
|
|
173
183
|
export type ToolExecutionSegment = {
|
|
174
184
|
type: 'tool_execution'
|
|
175
185
|
data: ToolExecutionData
|
|
@@ -208,7 +218,7 @@ export type ContextCompactionSegment = {
|
|
|
208
218
|
summary?: string
|
|
209
219
|
}
|
|
210
220
|
|
|
211
|
-
export type MessageSegment = TextSegment | ThinkingSegment | ToolExecutionSegment | ApprovalRequestSegment | ApprovalBatchSegment | ErrorSegment | ContextCompactionSegment
|
|
221
|
+
export type MessageSegment = TextSegment | ThinkingSegment | GuideSegment | ToolExecutionSegment | ApprovalRequestSegment | ApprovalBatchSegment | ErrorSegment | ContextCompactionSegment
|
|
212
222
|
|
|
213
223
|
export type MessageContent = string | MessageSegment[]
|
|
214
224
|
|
|
@@ -228,6 +238,11 @@ export interface ThinkingMessageData extends MessageDataBase {
|
|
|
228
238
|
text?: string
|
|
229
239
|
}
|
|
230
240
|
|
|
241
|
+
export interface GuideMessageData extends MessageDataBase {
|
|
242
|
+
type: 'GUIDE'
|
|
243
|
+
text?: string
|
|
244
|
+
}
|
|
245
|
+
|
|
231
246
|
export interface ExecutingToolMessageData extends MessageDataBase {
|
|
232
247
|
type: 'EXECUTING_TOOL'
|
|
233
248
|
integratedToolType?: string
|
|
@@ -309,6 +324,7 @@ export interface ContextCompactionEndMessageData extends MessageDataBase {
|
|
|
309
324
|
export type MessageData =
|
|
310
325
|
| TextMessageData
|
|
311
326
|
| ThinkingMessageData
|
|
327
|
+
| GuideMessageData
|
|
312
328
|
| ExecutingToolMessageData
|
|
313
329
|
| ExecutedToolMessageData
|
|
314
330
|
| ApprovalRequestMessageData
|
|
@@ -16,6 +16,7 @@ export type ParsedChunkAction =
|
|
|
16
16
|
| { action: 'metadata'; modelDisplayName: string; modelName: string; providerName: string; contextWindow: number }
|
|
17
17
|
| { action: 'text'; text: string }
|
|
18
18
|
| { action: 'thinking'; text: string }
|
|
19
|
+
| { action: 'guide'; text: string }
|
|
19
20
|
| { action: 'tool_execution'; segment: ToolExecutionSegment }
|
|
20
21
|
| { action: 'approval_request'; requestId: string; command: string; explanation?: string; approvalType: string }
|
|
21
22
|
| { action: 'approval_batch'; requestId: string; approvalType: string; toolCalls: PendingToolCallData[] }
|
|
@@ -65,6 +65,12 @@ export function parseChunkToAction(chunk: unknown): ParsedChunkAction | null {
|
|
|
65
65
|
}
|
|
66
66
|
return null
|
|
67
67
|
|
|
68
|
+
case MESSAGE_TYPE.GUIDE:
|
|
69
|
+
if (typeof data.text === 'string') {
|
|
70
|
+
return { action: 'guide', text: data.text }
|
|
71
|
+
}
|
|
72
|
+
return null
|
|
73
|
+
|
|
68
74
|
case MESSAGE_TYPE.EXECUTING_TOOL:
|
|
69
75
|
return {
|
|
70
76
|
action: 'tool_execution',
|
|
@@ -139,6 +139,22 @@ export class MessageSegmentAccumulator {
|
|
|
139
139
|
return this.getSegments()
|
|
140
140
|
}
|
|
141
141
|
|
|
142
|
+
/**
|
|
143
|
+
* Append guide text to the current message.
|
|
144
|
+
* If the last segment is guide, append to it; otherwise start a new guide segment.
|
|
145
|
+
*/
|
|
146
|
+
appendGuide(text: string): MessageSegment[] {
|
|
147
|
+
const lastSegment = this.segments[this.segments.length - 1]
|
|
148
|
+
|
|
149
|
+
if (lastSegment && lastSegment.type === 'guide') {
|
|
150
|
+
this.segments[this.segments.length - 1] = { type: 'guide', text: lastSegment.text + text }
|
|
151
|
+
} else {
|
|
152
|
+
this.segments.push({ type: 'guide', text })
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return this.getSegments()
|
|
156
|
+
}
|
|
157
|
+
|
|
142
158
|
/**
|
|
143
159
|
* Add a tool execution segment.
|
|
144
160
|
*
|
|
@@ -516,6 +532,9 @@ export class MessageSegmentAccumulator {
|
|
|
516
532
|
case 'thinking':
|
|
517
533
|
if (segment.text) this.appendThinking(segment.text)
|
|
518
534
|
break
|
|
535
|
+
case 'guide':
|
|
536
|
+
if (segment.text) this.appendGuide(segment.text)
|
|
537
|
+
break
|
|
519
538
|
case 'tool_execution':
|
|
520
539
|
this.addToolExecution(segment)
|
|
521
540
|
break
|
|
@@ -275,6 +275,12 @@ function processMessageData(
|
|
|
275
275
|
}
|
|
276
276
|
break
|
|
277
277
|
|
|
278
|
+
case MESSAGE_TYPE.GUIDE:
|
|
279
|
+
if ('text' in data && data.text) {
|
|
280
|
+
accumulator.appendGuide(data.text)
|
|
281
|
+
}
|
|
282
|
+
break
|
|
283
|
+
|
|
278
284
|
case MESSAGE_TYPE.EXECUTING_TOOL:
|
|
279
285
|
if ('integratedToolType' in data) {
|
|
280
286
|
accumulator.addToolExecution({
|
|
@@ -36,9 +36,12 @@ export const SelectButton = React.forwardRef<HTMLButtonElement, SelectButtonProp
|
|
|
36
36
|
"font-body text-left w-full",
|
|
37
37
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-focus",
|
|
38
38
|
"disabled:pointer-events-none disabled:opacity-50",
|
|
39
|
+
// Hover / active move the BACKGROUND only, like Input — the accent
|
|
40
|
+
// border is reserved for the selected state, so an unselected button
|
|
41
|
+
// no longer flashes "selected" under the cursor.
|
|
39
42
|
selected
|
|
40
43
|
? "bg-ods-open-yellow-light border-ods-accent hover:bg-ods-open-yellow-light-hover active:bg-ods-open-yellow-light-action"
|
|
41
|
-
: "bg-ods-card border-ods-border hover:bg-ods-bg-hover
|
|
44
|
+
: "bg-ods-card border-ods-border hover:bg-ods-bg-hover active:bg-ods-bg-active",
|
|
42
45
|
className,
|
|
43
46
|
)}
|
|
44
47
|
>
|
|
@@ -36,6 +36,12 @@
|
|
|
36
36
|
* `titleWrap` prop, forwarded to `TitleBlock`. Defaults to `false` (frozen
|
|
37
37
|
* single-line truncate). Content detail pages with CMS titles pass it to let
|
|
38
38
|
* long titles wrap instead of ellipsis-clip. Additive + default-preserving.
|
|
39
|
+
*
|
|
40
|
+
* SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL
|
|
41
|
+
* `subtitleRow` prop, forwarded to `TitleBlock`. Defaults to `'when-set'` (the
|
|
42
|
+
* subtitle row renders only when there is text for it). Pages whose subtitle is
|
|
43
|
+
* optional record data use it to choose how the row behaves while loading.
|
|
44
|
+
* Additive + default-preserving.
|
|
39
45
|
* ========================================================================== */
|
|
40
46
|
|
|
41
47
|
import React from 'react'
|
|
@@ -68,6 +74,12 @@ export interface PageLayoutProps {
|
|
|
68
74
|
/** When true, the title/subtitle render as line-box-accurate skeleton bars (forwarded to
|
|
69
75
|
* `TitleBlock`). Header height stays identical to the loaded state — for page skeletons. */
|
|
70
76
|
loading?: boolean
|
|
77
|
+
/** Render the header ACTIONS as placeholders (their set depends on data still loading). */
|
|
78
|
+
loadingActions?: boolean
|
|
79
|
+
/** When the subtitle row occupies the layout (forwarded to `TitleBlock`):
|
|
80
|
+
* `'when-set'` (default), `'while-loading'` (skeleton bar while loading, collapses
|
|
81
|
+
* if the record has no subtitle), or `'always'` (never collapses). */
|
|
82
|
+
subtitleRow?: 'when-set' | 'while-loading' | 'always'
|
|
71
83
|
/** When true, a long title wraps onto multiple lines instead of the frozen single-line
|
|
72
84
|
* ellipsis clamp (forwarded to `TitleBlock`). For content detail pages whose h1 is CMS
|
|
73
85
|
* data of arbitrary length. Additive + default-preserving. */
|
|
@@ -96,6 +108,8 @@ export function PageLayout({
|
|
|
96
108
|
titleSize,
|
|
97
109
|
titleAdornment,
|
|
98
110
|
loading,
|
|
111
|
+
loadingActions,
|
|
112
|
+
subtitleRow,
|
|
99
113
|
titleWrap,
|
|
100
114
|
}: PageLayoutProps) {
|
|
101
115
|
const hasActions = actions && actions.length > 0
|
|
@@ -118,6 +132,8 @@ export function PageLayout({
|
|
|
118
132
|
titleSize={titleSize}
|
|
119
133
|
titleAdornment={titleAdornment}
|
|
120
134
|
loading={loading}
|
|
135
|
+
loadingActions={loadingActions}
|
|
136
|
+
subtitleRow={subtitleRow}
|
|
121
137
|
titleWrap={titleWrap}
|
|
122
138
|
/>
|
|
123
139
|
)}
|
|
@@ -40,6 +40,14 @@
|
|
|
40
40
|
* byte-identical. Do NOT change defaults or touch anything else here.
|
|
41
41
|
*
|
|
42
42
|
* SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL
|
|
43
|
+
* `subtitleRow` prop. Defaults to `'when-set'` — the subtitle row keeps
|
|
44
|
+
* rendering only when `subtitle` is truthy, so every existing caller is
|
|
45
|
+
* byte-identical. The other two values decide how a page trades a header that
|
|
46
|
+
* never moves against a header that never shows an empty line; see the prop's
|
|
47
|
+
* own doc for which to pick. Additive + default-preserving; do NOT change the
|
|
48
|
+
* default.
|
|
49
|
+
*
|
|
50
|
+
* SANCTIONED EXCEPTION (2026-07, explicit human sign-off): the OPTIONAL
|
|
43
51
|
* `titleWrap` prop. Defaults to `false` — the frozen single-line `truncate`
|
|
44
52
|
* baseline is unchanged for every existing caller. Content DETAIL pages (whose
|
|
45
53
|
* h1 is CMS data of arbitrary length — releases, legal docs, FAQ docs, dev
|
|
@@ -58,12 +66,27 @@
|
|
|
58
66
|
* so its natural width drives the wrap while `truncate` still clamps a title
|
|
59
67
|
* that alone exceeds the container. The mobile (base) layout is untouched.
|
|
60
68
|
* This is the new frozen baseline; do NOT re-introduce breakpoint stacking.
|
|
69
|
+
*
|
|
70
|
+
* SANCTIONED EXCEPTION (2026-07, explicit human sign-off): overflow tooltips on
|
|
71
|
+
* the title and subtitle. This one CHANGES the baseline for every caller: the
|
|
72
|
+
* native `title=""` attribute on both is replaced by the ODS `FloatingTooltip`,
|
|
73
|
+
* armed only when the text is genuinely clipped (`useIsTruncated`). Both stay
|
|
74
|
+
* single-line — that is the design, and the tooltip is what makes a clipped
|
|
75
|
+
* title readable instead of merely pretty. Nothing is lost for assistive tech:
|
|
76
|
+
* `text-overflow: ellipsis` never hid the text from the accessibility tree, so
|
|
77
|
+
* the full string was always exposed; the attribute only ever served a mouse.
|
|
78
|
+
* The wrapper `div` the tooltip needs goes OUTSIDE the `h1`/`p` — never inside:
|
|
79
|
+
* a `div` inside a `p` is auto-closed by the HTML parser, which would split the
|
|
80
|
+
* subtitle paragraph and desync hydration. The `titleWrap` branch is untouched
|
|
81
|
+
* (wrapped text never clips, so it never needs a tooltip).
|
|
61
82
|
* ========================================================================== */
|
|
62
83
|
|
|
63
84
|
import React from 'react'
|
|
85
|
+
import { useIsTruncated } from '../../hooks/ui/use-is-truncated'
|
|
64
86
|
import { cn } from '../../utils/cn'
|
|
65
87
|
import type { ActionsMenuGroup } from '../ui/actions-menu'
|
|
66
88
|
import { EntityImage } from '../ui/entity-image'
|
|
89
|
+
import { FloatingTooltip } from '../ui/floating-tooltip'
|
|
67
90
|
import { PageActions, type PageActionButton } from '../ui/page-actions'
|
|
68
91
|
import { BackButton } from './back-button'
|
|
69
92
|
|
|
@@ -106,6 +129,28 @@ export interface TitleBlockProps {
|
|
|
106
129
|
* (typography + surrounding markup unchanged, so header height is identical to loaded).
|
|
107
130
|
* Additive + default-preserving: omit it and every existing caller is byte-identical. */
|
|
108
131
|
loading?: boolean
|
|
132
|
+
/**
|
|
133
|
+
* Render the ACTIONS as placeholders too. Separate from `loading` on purpose:
|
|
134
|
+
* a page whose action set is already final (it depends on the route, not on
|
|
135
|
+
* the record) keeps showing its real buttons while the title loads.
|
|
136
|
+
*/
|
|
137
|
+
loadingActions?: boolean
|
|
138
|
+
/**
|
|
139
|
+
* When the subtitle row occupies the layout. The subtitle is often optional
|
|
140
|
+
* RECORD data — unknown while loading, sometimes absent afterwards — and there
|
|
141
|
+
* is no setting that both fills the loading header and never leaves a blank
|
|
142
|
+
* line, so the caller picks which one matters.
|
|
143
|
+
*
|
|
144
|
+
* - `'when-set'` (default) — only when `subtitle` has text.
|
|
145
|
+
* - `'while-loading'` — also during `loading`, as a skeleton bar. The header
|
|
146
|
+
* looks complete while it loads, and the row collapses if the record turns
|
|
147
|
+
* out to have no subtitle (so that case settles one line shorter).
|
|
148
|
+
* - `'always'` — the row never collapses; an empty subtitle holds its line
|
|
149
|
+
* with an invisible spacer. The header cannot change height. Use it when the
|
|
150
|
+
* loaded page ALWAYS has a subtitle — typically a standalone skeleton
|
|
151
|
+
* component matching a page whose subtitle is guaranteed.
|
|
152
|
+
*/
|
|
153
|
+
subtitleRow?: 'when-set' | 'while-loading' | 'always'
|
|
109
154
|
/** When true, a long title WRAPS onto multiple lines instead of the frozen single-line
|
|
110
155
|
* ellipsis clamp. For content detail pages whose h1 is CMS data of arbitrary length.
|
|
111
156
|
* Additive + default-preserving: omit it and every existing caller is byte-identical. */
|
|
@@ -129,6 +174,22 @@ function TitleTextSkeleton({ widthClass, heightClass }: { widthClass: string; he
|
|
|
129
174
|
)
|
|
130
175
|
}
|
|
131
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Bar geometry per title size, following the ODS type scale across breakpoints
|
|
179
|
+
* (`md` = 800px, `lg` = 1280px here — the SAME widths the typography tokens
|
|
180
|
+
* switch at, so bar and text step together).
|
|
181
|
+
*
|
|
182
|
+
* `h2` needs no `lg` step because `text-h2` has none: it is 24/32 on mobile and
|
|
183
|
+
* 32/40 from tablet up. `text-h1` scales at both breakpoints (40 → 48 → 56), so
|
|
184
|
+
* its bar does too — reusing the h2 bar there would leave a stamp-sized smudge
|
|
185
|
+
* inside a 64px line. Heights stay well under the line box's ascent on purpose;
|
|
186
|
+
* see `TitleTextSkeleton` for why that is what keeps the header height honest.
|
|
187
|
+
*/
|
|
188
|
+
const TITLE_SKELETON_SIZE = {
|
|
189
|
+
h1: { width: 'w-56 md:w-80 lg:w-96', height: 'h-7 md:h-8 lg:h-9' },
|
|
190
|
+
h2: { width: 'w-48 md:w-72', height: 'h-4 md:h-6' },
|
|
191
|
+
} as const
|
|
192
|
+
|
|
132
193
|
export function TitleBlock({
|
|
133
194
|
title,
|
|
134
195
|
subtitle,
|
|
@@ -143,14 +204,28 @@ export function TitleBlock({
|
|
|
143
204
|
titleSize = 'h2',
|
|
144
205
|
titleAdornment,
|
|
145
206
|
loading,
|
|
207
|
+
loadingActions,
|
|
208
|
+
subtitleRow = 'when-set',
|
|
146
209
|
titleWrap = false,
|
|
147
210
|
}: TitleBlockProps) {
|
|
211
|
+
const hasSubtitleRow =
|
|
212
|
+
!!subtitle || subtitleRow === 'always' || (subtitleRow === 'while-loading' && !!loading)
|
|
148
213
|
const hasActions = actions && actions.length > 0
|
|
149
214
|
const hasMenuActions = !!menuActions && menuActions.some(g => g.items.length > 0)
|
|
150
215
|
const titleClass = titleSize === 'h1' ? 'text-h1' : 'text-h2'
|
|
151
216
|
// Frozen baseline is the single-line `truncate`; `titleWrap` swaps it for
|
|
152
217
|
// multi-line wrapping (break-words guards pathological unbroken tokens).
|
|
153
218
|
const titleOverflowClass = titleWrap ? 'break-words' : 'truncate'
|
|
219
|
+
const skeletonSize = TITLE_SKELETON_SIZE[titleSize]
|
|
220
|
+
|
|
221
|
+
// Exactly one title element renders per pass, so one ref covers every branch.
|
|
222
|
+
// The tooltip arms itself only on real clipping — repeating a fully visible
|
|
223
|
+
// title back to the user is noise, and `titleWrap`/loading never clip at all.
|
|
224
|
+
const { ref: titleRef, isTruncated: titleTruncated } = useIsTruncated<HTMLHeadingElement>(loading ? null : title)
|
|
225
|
+
const { ref: subtitleRef, isTruncated: subtitleTruncated } = useIsTruncated<HTMLParagraphElement>(
|
|
226
|
+
loading ? null : subtitle,
|
|
227
|
+
)
|
|
228
|
+
const titleNode = loading ? <TitleTextSkeleton widthClass={skeletonSize.width} heightClass={skeletonSize.height} /> : title
|
|
154
229
|
|
|
155
230
|
return (
|
|
156
231
|
<div
|
|
@@ -183,7 +258,7 @@ export function TitleBlock({
|
|
|
183
258
|
className="hidden md:inline-flex"
|
|
184
259
|
/>
|
|
185
260
|
)}
|
|
186
|
-
{(image ||
|
|
261
|
+
{(image || hasSubtitleRow || loading) ? (
|
|
187
262
|
<div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
|
|
188
263
|
{image && (
|
|
189
264
|
<EntityImage
|
|
@@ -196,15 +271,32 @@ export function TitleBlock({
|
|
|
196
271
|
{(loading || title) && (
|
|
197
272
|
titleAdornment ? (
|
|
198
273
|
<div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
|
|
199
|
-
|
|
274
|
+
{/* `min-w-0` moves onto the tooltip's wrapper: the wrapper is
|
|
275
|
+
now the flex item, and without it the row refuses to
|
|
276
|
+
shrink and pushes the adornment out. */}
|
|
277
|
+
<FloatingTooltip content={title} side="bottom" disabled={!titleTruncated} triggerClassName="min-w-0">
|
|
278
|
+
<h1 ref={titleRef} className={cn(titleClass, 'text-ods-text-primary min-w-0', titleOverflowClass)}>{titleNode}</h1>
|
|
279
|
+
</FloatingTooltip>
|
|
200
280
|
<span className="shrink-0">{titleAdornment}</span>
|
|
201
281
|
</div>
|
|
202
282
|
) : (
|
|
203
|
-
<
|
|
283
|
+
<FloatingTooltip content={title} side="bottom" disabled={!titleTruncated}>
|
|
284
|
+
<h1 ref={titleRef} className={cn(titleClass, 'text-ods-text-primary', titleOverflowClass)}>{titleNode}</h1>
|
|
285
|
+
</FloatingTooltip>
|
|
204
286
|
)
|
|
205
287
|
)}
|
|
206
|
-
{
|
|
207
|
-
|
|
288
|
+
{hasSubtitleRow && (
|
|
289
|
+
/* The NBSP is a pure spacer, reached only in `'always'` mode: an
|
|
290
|
+
empty `p` collapses to zero height (its only content would be
|
|
291
|
+
collapsible whitespace), which is exactly the shift that mode
|
|
292
|
+
exists to prevent. Hidden from AT — it carries no meaning. */
|
|
293
|
+
<FloatingTooltip content={subtitle} side="bottom" disabled={!subtitleTruncated}>
|
|
294
|
+
<p
|
|
295
|
+
ref={subtitleRef}
|
|
296
|
+
className="text-h6 text-ods-text-secondary truncate"
|
|
297
|
+
aria-hidden={!loading && !subtitle ? true : undefined}
|
|
298
|
+
>{loading ? <TitleTextSkeleton widthClass="w-28 md:w-36" heightClass="h-2.5 md:h-3" /> : (subtitle || '\u00A0')}</p>
|
|
299
|
+
</FloatingTooltip>
|
|
208
300
|
)}
|
|
209
301
|
</div>
|
|
210
302
|
</div>
|
|
@@ -212,7 +304,9 @@ export function TitleBlock({
|
|
|
212
304
|
title && (
|
|
213
305
|
titleAdornment ? (
|
|
214
306
|
<div className="flex items-center gap-[var(--spacing-system-m)] min-w-0 w-full">
|
|
215
|
-
<
|
|
307
|
+
<FloatingTooltip content={title} side="bottom" disabled={!titleTruncated} triggerClassName="min-w-0">
|
|
308
|
+
<h1 ref={titleRef} className={cn(titleClass, 'text-ods-text-primary min-w-0', titleOverflowClass)}>{title}</h1>
|
|
309
|
+
</FloatingTooltip>
|
|
216
310
|
<span className="shrink-0">{titleAdornment}</span>
|
|
217
311
|
</div>
|
|
218
312
|
) : (
|
|
@@ -226,13 +320,18 @@ export function TitleBlock({
|
|
|
226
320
|
)}
|
|
227
321
|
</div>
|
|
228
322
|
|
|
229
|
-
{
|
|
323
|
+
{/* `loadingActions` opens this gate on its own: the case the flag exists for
|
|
324
|
+
is a page that does not YET know its action set, which is exactly the
|
|
325
|
+
page that passes `actions={[]}` — gating on `hasActions` alone would
|
|
326
|
+
render nothing and then pop the real buttons in. */}
|
|
327
|
+
{(hasActions || hasMenuActions || selector || loadingActions) && (
|
|
230
328
|
<div className="flex gap-2 items-center shrink-0">
|
|
231
329
|
<PageActions
|
|
232
330
|
variant={actionsVariant}
|
|
233
331
|
actions={actions ?? []}
|
|
234
332
|
menuActions={menuActions}
|
|
235
333
|
selector={selector}
|
|
334
|
+
loading={loadingActions}
|
|
236
335
|
/>
|
|
237
336
|
</div>
|
|
238
337
|
)}
|