@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
|
@@ -81,6 +81,10 @@ const RadioGroupBlock = React.forwardRef<
|
|
|
81
81
|
<div className={cn("relative flex w-full flex-col", className)}>
|
|
82
82
|
<RadioGroupPrimitive.Root
|
|
83
83
|
ref={ref}
|
|
84
|
+
// Same marker every field exposes — it is how a form finds (and
|
|
85
|
+
// scrolls to) the first control that failed validation. The GROUP is
|
|
86
|
+
// the field here; the individual options are not separately invalid.
|
|
87
|
+
data-invalid={error ? true : undefined}
|
|
84
88
|
className={cn(
|
|
85
89
|
"w-full",
|
|
86
90
|
isGrouped
|
|
@@ -106,6 +110,10 @@ const RadioGroupBlock = React.forwardRef<
|
|
|
106
110
|
"flex w-full items-center gap-3",
|
|
107
111
|
"transition-colors duration-200",
|
|
108
112
|
isDisabled ? "cursor-not-allowed" : "cursor-pointer",
|
|
113
|
+
// Hover / active move the BACKGROUND only, like Input and
|
|
114
|
+
// CheckboxBlock — the border stays put so an unselected row
|
|
115
|
+
// never flashes "selected" under the cursor.
|
|
116
|
+
!isDisabled && "hover:bg-ods-bg-hover active:bg-ods-bg-active",
|
|
109
117
|
isGrouped
|
|
110
118
|
? cn(
|
|
111
119
|
"bg-ods-card px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs,8px)]",
|
|
@@ -114,12 +122,17 @@ const RadioGroupBlock = React.forwardRef<
|
|
|
114
122
|
)
|
|
115
123
|
: cn(
|
|
116
124
|
"rounded-[6px] border bg-ods-card p-[var(--spacing-system-sf)]",
|
|
117
|
-
!isDisabled && "hover:border-ods-accent/30",
|
|
118
125
|
!isDisabled &&
|
|
119
126
|
"has-[[data-state=checked]]:bg-[var(--ods-system-greys-soft-grey)]/30 has-[[data-state=checked]]:border-ods-accent/40",
|
|
120
127
|
error ? "border-ods-error" : "border-ods-border"
|
|
121
128
|
),
|
|
122
|
-
|
|
129
|
+
// Disabled matches a disabled field: flat `ods-bg` fill and
|
|
130
|
+
// greyed text, not a fade of the whole row. The greying lives
|
|
131
|
+
// on the TEXT COLUMN below, not here: as a descendant rule on
|
|
132
|
+
// the row it also repainted whatever `trailing` renders — a
|
|
133
|
+
// `Tag`'s label is a span too, and it went invisible against
|
|
134
|
+
// its own fill.
|
|
135
|
+
isDisabled && "bg-ods-bg",
|
|
123
136
|
itemClassName
|
|
124
137
|
)}
|
|
125
138
|
>
|
|
@@ -132,7 +145,10 @@ const RadioGroupBlock = React.forwardRef<
|
|
|
132
145
|
error ? "border-ods-error" : "border-[var(--color-border-strong)]",
|
|
133
146
|
"transition-colors duration-150",
|
|
134
147
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-2 focus-visible:ring-offset-ods-card",
|
|
148
|
+
// Disabled: the EMPTY dot dims with the text; a selected one
|
|
149
|
+
// keeps its accent so "on but locked" stays readable.
|
|
135
150
|
"disabled:cursor-not-allowed",
|
|
151
|
+
"disabled:data-[state=unchecked]:bg-ods-bg disabled:data-[state=unchecked]:border-ods-border",
|
|
136
152
|
"data-[state=checked]:border-ods-accent"
|
|
137
153
|
)}
|
|
138
154
|
>
|
|
@@ -140,7 +156,16 @@ const RadioGroupBlock = React.forwardRef<
|
|
|
140
156
|
<span className="block h-1/2 w-1/2 rounded-full bg-ods-accent" />
|
|
141
157
|
</RadioGroupPrimitive.Indicator>
|
|
142
158
|
</RadioGroupPrimitive.Item>
|
|
143
|
-
<div
|
|
159
|
+
<div
|
|
160
|
+
className={cn(
|
|
161
|
+
"flex min-w-0 flex-1 flex-col justify-center",
|
|
162
|
+
// Direct children only — these are the two component-owned
|
|
163
|
+
// wrappers. `option.label` / `option.description` are
|
|
164
|
+
// ReactNode, so a descendant rule would repaint content the
|
|
165
|
+
// caller styled itself.
|
|
166
|
+
isDisabled && "[&>span]:text-ods-text-disabled",
|
|
167
|
+
)}
|
|
168
|
+
>
|
|
144
169
|
<span
|
|
145
170
|
className={cn(
|
|
146
171
|
"font-[family-name:var(--font-h4-family)] font-[number:var(--font-h4-weight)] text-[length:var(--font-size-h4-body)] leading-[24px]",
|
|
@@ -106,7 +106,8 @@ const innerInputStyles = cn(
|
|
|
106
106
|
"flex-1 min-w-[60px] bg-transparent border-none outline-none",
|
|
107
107
|
"text-h4",
|
|
108
108
|
"text-ods-text-primary placeholder:text-ods-text-secondary",
|
|
109
|
-
|
|
109
|
+
// Disabled - match Input exactly (value greys out, placeholder dims further)
|
|
110
|
+
"disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border",
|
|
110
111
|
"touch-manipulation"
|
|
111
112
|
)
|
|
112
113
|
|
|
@@ -42,7 +42,14 @@ const SelectTrigger = React.forwardRef<
|
|
|
42
42
|
"enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-active",
|
|
43
43
|
!isInvalid && "data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent",
|
|
44
44
|
"group",
|
|
45
|
+
// Disabled - match Input exactly: value greys out, placeholder dims
|
|
46
|
+
// further. The `data-[placeholder]` rule above is a class+attribute
|
|
47
|
+
// selector, so the disabled placeholder rule stacks `:disabled` on top
|
|
48
|
+
// of it to win on specificity rather than on source order.
|
|
45
49
|
"disabled:!cursor-not-allowed disabled:bg-ods-bg",
|
|
50
|
+
"disabled:text-ods-text-disabled disabled:data-[placeholder]:text-ods-border",
|
|
51
|
+
// The chevron sets its own colour — grey it with the value.
|
|
52
|
+
"disabled:[&_svg]:text-ods-text-disabled",
|
|
46
53
|
"transition-colors duration-200 cursor-pointer",
|
|
47
54
|
"[&>span]:line-clamp-1",
|
|
48
55
|
isInvalid && "border-ods-error enabled:hover:border-ods-error enabled:active:border-ods-error data-[state=open]:border-ods-error",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import React, { useState, useEffect, useMemo, useRef, useCallback } from 'react'
|
|
3
|
+
import React, { useState, useEffect, useDeferredValue, useMemo, useRef, useCallback } from 'react'
|
|
4
4
|
import { useSearchParams, useRouter, usePathname } from '../../embed-shims/next-navigation'
|
|
5
5
|
import { cn } from '../../utils/cn'
|
|
6
6
|
|
|
@@ -38,8 +38,14 @@ interface TabNavigationProps {
|
|
|
38
38
|
urlSync?: boolean | TabNavigationUrlSyncOptions
|
|
39
39
|
defaultTab?: string // Fallback when no valid tab in URL or initial value
|
|
40
40
|
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
/**
|
|
42
|
+
* Render prop for the tab BODY. Receives the tab to render and, additively,
|
|
43
|
+
* a `isStale` flag — true while the bar has already moved to a newly clicked
|
|
44
|
+
* tab but the body is still showing the PREVIOUS one (see `deferredActiveTab`
|
|
45
|
+
* below). Use it to dim the body or show a pending hint; without it a slow
|
|
46
|
+
* tab looks like a tab that simply did nothing.
|
|
47
|
+
*/
|
|
48
|
+
children?: (activeTab: string, state: { isStale: boolean }) => React.ReactNode
|
|
43
49
|
}
|
|
44
50
|
|
|
45
51
|
export function TabNavigation({
|
|
@@ -119,6 +125,22 @@ export function TabNavigation({
|
|
|
119
125
|
}
|
|
120
126
|
}
|
|
121
127
|
|
|
128
|
+
// The tab BAR follows the click immediately; the tab BODY is deferred, which
|
|
129
|
+
// makes React treat swapping it as a transition. That is the whole fix for the
|
|
130
|
+
// flash: a tab whose data is not in the client cache yet SUSPENDS the moment it
|
|
131
|
+
// mounts, and outside a transition React has to show that Suspense fallback —
|
|
132
|
+
// so the skeleton appears for a frame before the real content lands. Inside a
|
|
133
|
+
// transition React keeps the previous tab on screen and swaps only once the new
|
|
134
|
+
// one is ready. Deferring the body rather than the whole state keeps the
|
|
135
|
+
// underline responsive, so the click still feels instant.
|
|
136
|
+
//
|
|
137
|
+
// The cost of that trade is that a SLOW tab leaves the previous tab's content
|
|
138
|
+
// under the new tab's underline with nothing saying so — worse than a skeleton,
|
|
139
|
+
// because stale data reads as fresh data. `isStale` is handed to the render prop
|
|
140
|
+
// so the body can mark itself while it waits.
|
|
141
|
+
const deferredActiveTab = useDeferredValue(activeTab)
|
|
142
|
+
const isStale = activeTab !== deferredActiveTab
|
|
143
|
+
|
|
122
144
|
const scrollRef = useRef<HTMLDivElement>(null)
|
|
123
145
|
const activeTabRef = useRef<HTMLButtonElement>(null)
|
|
124
146
|
const isFirstActiveScrollRef = useRef(true)
|
|
@@ -291,7 +313,7 @@ export function TabNavigation({
|
|
|
291
313
|
</div>
|
|
292
314
|
|
|
293
315
|
{/* Render children with active tab if provided */}
|
|
294
|
-
{children && children(
|
|
316
|
+
{children && children(deferredActiveTab, { isStale })}
|
|
295
317
|
</>
|
|
296
318
|
)
|
|
297
319
|
}
|
|
@@ -140,7 +140,13 @@ export function TagSearchInput<T = string>({
|
|
|
140
140
|
"has-[:focus]:border-ods-accent",
|
|
141
141
|
!disabled && "hover:bg-ods-bg-hover hover:border-ods-border-hover",
|
|
142
142
|
disabled && "bg-ods-bg",
|
|
143
|
-
disabled && "cursor-not-allowed
|
|
143
|
+
disabled && "cursor-not-allowed",
|
|
144
|
+
// Match Input: grey the text and the icons, don't fade the whole
|
|
145
|
+
// control (a blanket opacity also washes out the border). Icons only —
|
|
146
|
+
// this container has no adornment spans of its own, so a `[&_span]`
|
|
147
|
+
// rule would ONLY have reached the selected `Tag` chips, whose label is
|
|
148
|
+
// a span, and greyed them into their own fill in the light theme.
|
|
149
|
+
"has-[:disabled]:[&_svg]:text-ods-text-disabled",
|
|
144
150
|
className,
|
|
145
151
|
)}
|
|
146
152
|
onClick={() => {
|
|
@@ -199,7 +205,8 @@ export function TagSearchInput<T = string>({
|
|
|
199
205
|
"flex-1 min-w-[60px] bg-transparent border-none outline-none",
|
|
200
206
|
"text-h4",
|
|
201
207
|
"text-ods-text-primary placeholder:text-ods-text-secondary",
|
|
202
|
-
|
|
208
|
+
// Disabled - match Input exactly (value greys out, placeholder dims further)
|
|
209
|
+
"disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border",
|
|
203
210
|
)}
|
|
204
211
|
/>
|
|
205
212
|
</div>
|
|
@@ -119,6 +119,12 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
119
119
|
!hideBorder && cn("rounded-[6px] border bg-ods-card border-ods-border", accentBorder),
|
|
120
120
|
!hideBorder && !props.disabled && "hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-active",
|
|
121
121
|
!hideBorder && props.disabled && "!cursor-not-allowed bg-ods-bg",
|
|
122
|
+
// Start / end icons carry their own colour — grey them with the value
|
|
123
|
+
// so a disabled field reads as one flat colour. Scoped to the DIRECT
|
|
124
|
+
// span children (the two icon wrappers): the nodes inside them are
|
|
125
|
+
// rendered verbatim so callers can pass an interactive trigger and own
|
|
126
|
+
// its styling, and a descendant rule would overwrite exactly that.
|
|
127
|
+
"has-[:disabled]:[&>span]:text-ods-text-disabled has-[:disabled]:[&_svg]:text-ods-text-disabled",
|
|
122
128
|
!hideBorder && isInvalid && cn("border-ods-error hover:border-ods-error", accentBorderError),
|
|
123
129
|
hideBorder && "bg-transparent",
|
|
124
130
|
)}
|
|
@@ -150,7 +156,8 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
|
|
|
150
156
|
// internally instead of pushing the icon out of frame.
|
|
151
157
|
"max-h-[160px] overflow-y-auto",
|
|
152
158
|
"scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
|
|
153
|
-
|
|
159
|
+
// Disabled - match Input exactly (value greys out, placeholder dims further)
|
|
160
|
+
"disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border",
|
|
154
161
|
className,
|
|
155
162
|
)}
|
|
156
163
|
{...props}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use client'
|
|
2
2
|
|
|
3
|
-
import
|
|
3
|
+
import { type ReactNode } from 'react'
|
|
4
|
+
import { useIsTruncated } from '../../hooks/ui/use-is-truncated'
|
|
4
5
|
import { cn } from '../../utils/cn'
|
|
5
6
|
import { FloatingTooltip } from './floating-tooltip'
|
|
6
7
|
|
|
@@ -69,24 +70,8 @@ export function TruncateText({
|
|
|
69
70
|
tone = 'primary',
|
|
70
71
|
mono = false,
|
|
71
72
|
}: TruncateTextProps) {
|
|
72
|
-
const ref = useRef<HTMLSpanElement>(null)
|
|
73
|
-
const [isTruncated, setIsTruncated] = useState(false)
|
|
74
73
|
const isMultiLine = lines > 1
|
|
75
|
-
|
|
76
|
-
useEffect(() => {
|
|
77
|
-
const el = ref.current
|
|
78
|
-
if (!el) return
|
|
79
|
-
const check = () => {
|
|
80
|
-
const overflows = isMultiLine
|
|
81
|
-
? el.scrollHeight > el.clientHeight + 1
|
|
82
|
-
: el.scrollWidth > el.clientWidth + 1
|
|
83
|
-
setIsTruncated(overflows)
|
|
84
|
-
}
|
|
85
|
-
check()
|
|
86
|
-
const ro = new ResizeObserver(check)
|
|
87
|
-
ro.observe(el)
|
|
88
|
-
return () => ro.disconnect()
|
|
89
|
-
}, [children, isMultiLine])
|
|
74
|
+
const { ref, isTruncated } = useIsTruncated<HTMLSpanElement>(children, { multiline: isMultiLine })
|
|
90
75
|
|
|
91
76
|
const clampClass = isMultiLine
|
|
92
77
|
? LINE_CLAMP_CLASS[lines as Exclude<typeof lines, 1>]
|
package/src/hooks/ui/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './use-focus-trap'
|
|
|
6
6
|
export * from './use-header-height'
|
|
7
7
|
export * from './use-horizontal-scrollbar'
|
|
8
8
|
export * from './use-image-edge-color'
|
|
9
|
+
export * from './use-is-truncated'
|
|
9
10
|
export * from './use-local-storage'
|
|
10
11
|
export * from './use-marquee-engine'
|
|
11
12
|
export * from './use-media-query'
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client'
|
|
2
|
+
|
|
3
|
+
import { useCallback, useEffect, useState } from 'react'
|
|
4
|
+
|
|
5
|
+
export interface UseIsTruncatedOptions {
|
|
6
|
+
/** Measure vertical clipping (`line-clamp-N`) instead of the single-line ellipsis. */
|
|
7
|
+
multiline?: boolean
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Tracks whether an element's text is ACTUALLY clipped — the condition for
|
|
12
|
+
* offering a "full value" tooltip. Text that fits needs no tooltip, and showing
|
|
13
|
+
* one anyway just repeats what the user is already reading.
|
|
14
|
+
*
|
|
15
|
+
* Three things can change the answer, and all three are watched: the box
|
|
16
|
+
* resizing (`ResizeObserver`), the text itself changing, and the measured
|
|
17
|
+
* ELEMENT changing. None of them implies the others — new text inside a
|
|
18
|
+
* width-constrained box leaves the box exactly as it was, so the observer never
|
|
19
|
+
* fires; and a caller that swaps between render branches (adornment vs plain)
|
|
20
|
+
* mounts a different node under the same text, which a `useRef` would leave
|
|
21
|
+
* measured against the old, detached node. Pass the rendered value as `value`.
|
|
22
|
+
*
|
|
23
|
+
* `ref` is a CALLBACK ref, not a `RefObject` — that is what makes the element
|
|
24
|
+
* itself a dependency. Attach it exactly like a normal ref; there is no
|
|
25
|
+
* `.current` to read.
|
|
26
|
+
*
|
|
27
|
+
* ```tsx
|
|
28
|
+
* const { ref, isTruncated } = useIsTruncated<HTMLHeadingElement>(title)
|
|
29
|
+
* <FloatingTooltip content={title} disabled={!isTruncated}>
|
|
30
|
+
* <h1 ref={ref} className="truncate">{title}</h1>
|
|
31
|
+
* </FloatingTooltip>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
export function useIsTruncated<T extends HTMLElement>(value: unknown, options?: UseIsTruncatedOptions) {
|
|
35
|
+
const multiline = options?.multiline ?? false
|
|
36
|
+
const [element, setElement] = useState<T | null>(null)
|
|
37
|
+
const [isTruncated, setIsTruncated] = useState(false)
|
|
38
|
+
|
|
39
|
+
// Wrapped so the identity stays stable across renders: passing the raw
|
|
40
|
+
// `useState` setter as a ref would be stable too, but this keeps the public
|
|
41
|
+
// shape a plain `RefCallback<T>` instead of a state dispatcher.
|
|
42
|
+
const ref = useCallback((node: T | null) => setElement(node), [])
|
|
43
|
+
|
|
44
|
+
useEffect(() => {
|
|
45
|
+
if (!element) {
|
|
46
|
+
setIsTruncated(false)
|
|
47
|
+
return
|
|
48
|
+
}
|
|
49
|
+
// The 1px tolerance absorbs sub-pixel layout rounding, which otherwise
|
|
50
|
+
// reports a permanent fractional "overflow" at some zoom levels / DPRs and
|
|
51
|
+
// leaves a tooltip armed on text that is fully visible.
|
|
52
|
+
const check = () => {
|
|
53
|
+
setIsTruncated(
|
|
54
|
+
multiline
|
|
55
|
+
? element.scrollHeight > element.clientHeight + 1
|
|
56
|
+
: element.scrollWidth > element.clientWidth + 1,
|
|
57
|
+
)
|
|
58
|
+
}
|
|
59
|
+
check()
|
|
60
|
+
const observer = new ResizeObserver(check)
|
|
61
|
+
observer.observe(element)
|
|
62
|
+
return () => observer.disconnect()
|
|
63
|
+
}, [element, value, multiline])
|
|
64
|
+
|
|
65
|
+
return { ref, isTruncated }
|
|
66
|
+
}
|
|
@@ -183,7 +183,50 @@ export const PendingApprovalSegment: Story = {
|
|
|
183
183
|
},
|
|
184
184
|
};
|
|
185
185
|
|
|
186
|
-
/**
|
|
186
|
+
/**
|
|
187
|
+
* `[card://]` markers inside a `guide` segment resolve exactly as they do in a
|
|
188
|
+
* text segment — the host's `renderEntityCard` node is hoisted out of the
|
|
189
|
+
* markdown as a sibling block, and an unknown id degrades to the bare id.
|
|
190
|
+
* Regression guard: the guide body used to skip the marker plan entirely.
|
|
191
|
+
*/
|
|
192
|
+
export const GuideWithEntityCards: Story = {
|
|
193
|
+
args: {
|
|
194
|
+
role: "assistant",
|
|
195
|
+
name: "Mingo",
|
|
196
|
+
assistantType: "mingo",
|
|
197
|
+
timestamp: at247(),
|
|
198
|
+
chatRefs: {
|
|
199
|
+
"onboarding_guide:first-check": {
|
|
200
|
+
type: "onboarding_guide",
|
|
201
|
+
id: "first-check",
|
|
202
|
+
title: "Create Your First Monitoring Check",
|
|
203
|
+
url: "/help-center/onboarding-guides/first-check",
|
|
204
|
+
},
|
|
205
|
+
"onboarding_guide:add-devices": {
|
|
206
|
+
type: "onboarding_guide",
|
|
207
|
+
id: "add-devices",
|
|
208
|
+
title: "Add Devices to a Policy",
|
|
209
|
+
url: "/help-center/onboarding-guides/add-devices",
|
|
210
|
+
},
|
|
211
|
+
},
|
|
212
|
+
renderEntityCard: (ref) =>
|
|
213
|
+
ref.type === "onboarding_guide" ? (
|
|
214
|
+
<div className="flex items-center gap-[var(--spacing-system-s)] rounded-md border border-ods-border bg-ods-card p-[var(--spacing-system-s)]">
|
|
215
|
+
<span className="flex-1 text-h4 text-ods-text-primary">{ref.title}</span>
|
|
216
|
+
<span className="text-h5 text-ods-text-secondary">Guide</span>
|
|
217
|
+
</div>
|
|
218
|
+
) : null,
|
|
219
|
+
content: [
|
|
220
|
+
{
|
|
221
|
+
type: "guide",
|
|
222
|
+
text: "Here's how to create a monitoring policy (check) in OpenFrame:\n\n[card://onboarding_guide:first-check]\n\nStep-by-step:\n\n1. Go to Monitoring → Policies → Add Policy\n2. Write your osquery SQL query\n3. Save the policy",
|
|
223
|
+
},
|
|
224
|
+
{ type: "text", text: "A plain text segment renders its own marker identically:\n\n[card://onboarding_guide:add-devices]" },
|
|
225
|
+
] satisfies MessageSegment[],
|
|
226
|
+
},
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/** Every remaining segment type in one message — thinking, guide, tool execution, context compaction, error. */
|
|
187
230
|
export const AllSegmentTypes: Story = {
|
|
188
231
|
args: {
|
|
189
232
|
role: "assistant",
|
|
@@ -195,6 +238,10 @@ export const AllSegmentTypes: Story = {
|
|
|
195
238
|
text: "The user wants Slack installed. Homebrew is present on the endpoint, so `brew install --cask slack` is the cleanest path.",
|
|
196
239
|
},
|
|
197
240
|
{ type: "text", text: "Installing Slack now." },
|
|
241
|
+
{
|
|
242
|
+
type: "guide",
|
|
243
|
+
text: "Here's how to create a monitoring policy (check) in OpenFrame:\n\nStep-by-step:\n\n1. Go to Monitoring → Policies → Add Policy\n2. Give it a Name (something descriptive)\n3. Write your osquery SQL query — the query should return rows for devices that fail the check\n4. Set the interval — how often each device runs the check\n5. Save the policy",
|
|
244
|
+
},
|
|
198
245
|
{
|
|
199
246
|
type: "tool_execution",
|
|
200
247
|
data: {
|