@flamingo-stack/openframe-frontend-core 0.0.741 → 0.0.743
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/build/export-map.json +1 -1
- package/dist/{chunk-SFY4UYPY.cjs → chunk-4A6OF4HI.cjs} +44 -44
- package/dist/{chunk-VA7OMDZO.cjs → chunk-4FWURR3W.cjs} +50 -50
- package/dist/{chunk-WBANGORJ.cjs → chunk-4N7KGOBS.cjs} +3 -3
- package/dist/{chunk-PQ2M7P2U.cjs → chunk-5TCIRWHL.cjs} +9 -9
- package/dist/{chunk-FQN6STIM.cjs → chunk-6XCQXGQA.cjs} +20 -20
- package/dist/{chunk-BV5WZVOP.cjs → chunk-7YDF3QBN.cjs} +258 -257
- package/dist/{chunk-7ZFCR7UY.cjs → chunk-ABLUMGCI.cjs} +3 -3
- package/dist/{chunk-PX7FY655.cjs → chunk-ASMNNSUQ.cjs} +3 -3
- package/dist/{chunk-CWRVTQUO.cjs → chunk-BM6JZRVD.cjs} +101 -101
- package/dist/{chunk-LZ6NOVXX.cjs → chunk-BNTELSHZ.cjs} +103 -103
- package/dist/{chunk-SA7LDEVJ.cjs → chunk-CSJO7ZGN.cjs} +3 -3
- package/dist/{chunk-VOX4E6AX.cjs → chunk-DGFDBPO7.cjs} +7 -5
- package/dist/{chunk-KLI4OSRA.cjs → chunk-GAGNKWOC.cjs} +2 -2
- package/dist/{chunk-QEUGYHFB.cjs → chunk-IRWJKVVZ.cjs} +20 -20
- package/dist/{chunk-YRE3O65E.cjs → chunk-JGZEHBBD.cjs} +12 -12
- package/dist/{chunk-FICJS5OA.cjs → chunk-KTPR66EE.cjs} +4 -4
- package/dist/{chunk-YXKB5WRE.cjs → chunk-LJ46XYYP.cjs} +60 -48
- package/dist/{chunk-AAFW4STC.cjs → chunk-LU7TD6QS.cjs} +5 -5
- package/dist/{chunk-JFS6TZDX.cjs → chunk-MLZKPDPZ.cjs} +64 -64
- package/dist/{chunk-EKGL5K5K.cjs → chunk-MT7ELGCV.cjs} +17 -17
- package/dist/{chunk-HYBMADYO.cjs → chunk-NGUJVM6O.cjs} +22 -22
- package/dist/{chunk-3HPB4H4C.cjs → chunk-NTGIQP7O.cjs} +7 -7
- package/dist/{chunk-GABDE3FM.cjs → chunk-OWGLRE74.cjs} +19 -19
- package/dist/{chunk-IFS4AH77.cjs → chunk-QYLIHZKZ.cjs} +10 -10
- package/dist/{chunk-GVTDVS5U.cjs → chunk-R34JD6NP.cjs} +3 -3
- package/dist/{chunk-YQ37ZCD2.cjs → chunk-SQHTX5XP.cjs} +16 -16
- package/dist/{chunk-XJ36TEGE.cjs → chunk-TKASH5YC.cjs} +47 -47
- package/dist/{chunk-DSVNFRY5.cjs → chunk-U2O5YQRP.cjs} +38 -38
- package/dist/{chunk-5SVOGLNK.cjs → chunk-UL6ZZSL4.cjs} +6 -6
- package/dist/{chunk-O67V3HJR.cjs → chunk-URVIPSAJ.cjs} +24 -24
- package/dist/{chunk-GSWQZX23.cjs → chunk-UY6A6TEY.cjs} +2 -2
- package/dist/{chunk-DUVOQHQI.cjs → chunk-W2RM764D.cjs} +7 -7
- package/dist/{chunk-IK2RJUEA.cjs → chunk-WCY35GB5.cjs} +845 -810
- package/dist/{chunk-NTJE6SM6.cjs → chunk-WI2OTGKC.cjs} +2 -2
- package/dist/{chunk-ITPEFCQ2.cjs → chunk-XJO5ZHO6.cjs} +32 -32
- package/dist/{chunk-I6UE6YVM.cjs → chunk-Y2FN3E32.cjs} +6 -6
- package/dist/{chunk-4XL4YEZM.cjs → chunk-YDFKFLDQ.cjs} +8 -8
- package/dist/{chunk-HF7Z2E4W.cjs → chunk-YIOWV7LS.cjs} +2 -2
- package/dist/{chunk-B3XBJCOD.cjs → chunk-YJRMCFX3.cjs} +7 -7
- package/dist/{chunk-ZYOS7LVQ.cjs → chunk-YWMUZLU2.cjs} +89 -89
- package/dist/{chunk-ZJPDGVA6.cjs → chunk-ZP5DFF3Q.cjs} +28 -28
- package/dist/{cloud-tenants-MVJK52I2.cjs → cloud-tenants-DZC3ACG6.cjs} +24 -24
- package/dist/components/case-studies/index.cjs +37 -37
- package/dist/components/chat/entity-cards/onboarding-guide-card.d.ts.map +1 -1
- package/dist/components/chat/entity-cards/onboarding-guide-card.js +22 -12
- package/dist/components/chat/index.cjs +17 -17
- package/dist/components/chat/tool-execution-display.d.ts.map +1 -1
- package/dist/components/chat/tool-execution-display.js +5 -4
- package/dist/components/contact/index.cjs +25 -25
- package/dist/components/docs/index.cjs +28 -28
- package/dist/components/embeds/index.cjs +25 -25
- package/dist/components/faq/index.cjs +26 -26
- package/dist/components/features/index.cjs +24 -24
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts +1 -1
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.d.ts.map +1 -1
- package/dist/components/features/scripts/schedule-form/schedule-interval-input.js +15 -13
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.d.ts.map +1 -1
- package/dist/components/features/scripts/schedule-form/schedule-offline-fields.js +9 -7
- package/dist/components/help-center-pages/index.cjs +144 -144
- package/dist/components/index.cjs +87 -85
- package/dist/components/meeting-scheduler/booking-form.d.ts +11 -1
- package/dist/components/meeting-scheduler/booking-form.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/booking-form.js +15 -3
- package/dist/components/meeting-scheduler/index.cjs +452 -158
- package/dist/components/meeting-scheduler/index.d.ts +16 -1
- package/dist/components/meeting-scheduler/index.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/index.js +157 -5
- package/dist/components/meeting-scheduler/modal-header.d.ts +27 -0
- package/dist/components/meeting-scheduler/modal-header.d.ts.map +1 -0
- package/dist/components/meeting-scheduler/modal-header.js +104 -0
- package/dist/components/meeting-scheduler/slot-picker.d.ts +13 -2
- package/dist/components/meeting-scheduler/slot-picker.d.ts.map +1 -1
- package/dist/components/meeting-scheduler/slot-picker.js +112 -73
- package/dist/components/navigation/index.cjs +27 -27
- package/dist/components/onboarding-guides/index.cjs +27 -27
- package/dist/components/product-demo/index.cjs +9 -9
- package/dist/components/related-content/index.cjs +12 -12
- package/dist/components/shared/product-release/product-release-card.d.ts.map +1 -1
- package/dist/components/shared/product-release/product-release-card.js +2 -0
- package/dist/components/tickets/index.cjs +30 -30
- package/dist/components/ui/index.cjs +26 -24
- package/dist/components/ui/radio-group.d.ts +6 -0
- package/dist/components/ui/radio-group.d.ts.map +1 -1
- package/dist/components/ui/radio-group.js +65 -58
- package/dist/components/ui/tab-navigation.d.ts +19 -1
- package/dist/components/ui/tab-navigation.d.ts.map +1 -1
- package/dist/components/ui/tab-navigation.js +32 -8
- package/dist/{devices-W3S7Q2PG.cjs → devices-MFGE2JQV.cjs} +25 -25
- package/dist/{embeddable-chat-6JKKFQEB.cjs → embeddable-chat-7ZEGCDVF.cjs} +92 -92
- package/dist/hooks/index.cjs +10 -10
- package/dist/index.cjs +32 -30
- package/dist/{knowledge-AIYQ4FC3.cjs → knowledge-7VPS2ZBU.cjs} +25 -25
- package/dist/{logs-7SHJ2ARE.cjs → logs-M3QPAUH7.cjs} +32 -32
- package/dist/{policies-EIZJ5SUP.cjs → policies-UAVE6X62.cjs} +28 -28
- package/dist/{remote-session-BZMNE6D2.cjs → remote-session-N4PJB5FW.cjs} +10 -10
- package/dist/{scripts-J2FWC7E4.cjs → scripts-EDQ3IQKW.cjs} +31 -31
- package/dist/{software-update-QAMNASVF.cjs → software-update-B2MZVNPK.cjs} +28 -28
- package/dist/{tickets-board-CEDHD3KB.cjs → tickets-board-O753S4ZB.cjs} +14 -14
- package/dist/utils/embed-authed-fetch.d.ts +33 -0
- package/dist/utils/embed-authed-fetch.d.ts.map +1 -1
- package/dist/utils/embed-authed-fetch.js +3 -1
- package/dist/utils/index.cjs +3 -1
- package/dist/{video-file-player-ZH2ILAPO.cjs → video-file-player-QZ3YOLHO.cjs} +2 -2
- package/package.json +1 -1
- package/src/components/chat/entity-cards/onboarding-guide-card.tsx +8 -1
- package/src/components/chat/tool-execution-display.tsx +21 -10
- package/src/components/features/scripts/schedule-form/schedule-interval-input.tsx +20 -13
- package/src/components/features/scripts/schedule-form/schedule-offline-fields.tsx +9 -5
- package/src/components/meeting-scheduler/booking-form.tsx +29 -3
- package/src/components/meeting-scheduler/index.tsx +224 -3
- package/src/components/meeting-scheduler/modal-header.tsx +145 -0
- package/src/components/meeting-scheduler/slot-picker.tsx +72 -8
- package/src/components/shared/product-release/product-release-card.tsx +2 -0
- package/src/components/ui/radio-group.tsx +51 -36
- package/src/components/ui/tab-navigation.tsx +54 -5
- package/src/utils/embed-authed-fetch.ts +42 -1
|
@@ -47,6 +47,12 @@ interface RadioGroupBlockOption {
|
|
|
47
47
|
disabled?: boolean;
|
|
48
48
|
/** Optional trailing slot rendered at the end of the row (e.g. a discount tag) */
|
|
49
49
|
trailing?: ReactNode;
|
|
50
|
+
/**
|
|
51
|
+
* Optional content under the option's row, inside the option and across its
|
|
52
|
+
* full width (e.g. the fields the option needs on a narrow screen). Rendered
|
|
53
|
+
* as given, with no wrapper, so a footer hidden at some widths leaves no gap.
|
|
54
|
+
*/
|
|
55
|
+
footer?: ReactNode;
|
|
50
56
|
}
|
|
51
57
|
|
|
52
58
|
interface RadioGroupBlockProps extends Omit<ComponentPropsWithoutRef<typeof RadioGroupPrimitive.Root>, 'children'> {
|
|
@@ -99,42 +105,9 @@ const RadioGroupBlock = forwardRef<ComponentRef<typeof RadioGroupPrimitive.Root>
|
|
|
99
105
|
const itemId = `${props.name ?? 'radio-block'}-${option.value}`;
|
|
100
106
|
const isDisabled = disabled || option.disabled;
|
|
101
107
|
const isLast = index === options.length - 1;
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
htmlFor={itemId}
|
|
106
|
-
aria-disabled={isDisabled || undefined}
|
|
107
|
-
className={cn(
|
|
108
|
-
'flex w-full items-center gap-3',
|
|
109
|
-
isHorizontal && 'min-w-0 flex-1',
|
|
110
|
-
'transition-colors duration-200',
|
|
111
|
-
isDisabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
112
|
-
// Hover / active move the BACKGROUND only, like Input and
|
|
113
|
-
// CheckboxBlock — the border stays put so an unselected row
|
|
114
|
-
// never flashes "selected" under the cursor.
|
|
115
|
-
!isDisabled && 'hover:bg-ods-bg-hover active:bg-ods-bg-active',
|
|
116
|
-
isGrouped
|
|
117
|
-
? cn(
|
|
118
|
-
'bg-ods-card px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs,8px)]',
|
|
119
|
-
!isLast && (isHorizontal ? 'border-r' : 'border-b'),
|
|
120
|
-
error ? 'border-ods-error' : 'border-ods-border',
|
|
121
|
-
)
|
|
122
|
-
: cn(
|
|
123
|
-
'rounded-[6px] border bg-ods-card p-[var(--spacing-system-sf)]',
|
|
124
|
-
!isDisabled &&
|
|
125
|
-
'has-[[data-state=checked]]:bg-[var(--ods-system-greys-soft-grey)]/30 has-[[data-state=checked]]:border-ods-accent/40',
|
|
126
|
-
error ? 'border-ods-error' : 'border-ods-border',
|
|
127
|
-
),
|
|
128
|
-
// Disabled matches a disabled field: flat `ods-bg` fill and
|
|
129
|
-
// greyed text, not a fade of the whole row. The greying lives
|
|
130
|
-
// on the TEXT COLUMN below, not here: as a descendant rule on
|
|
131
|
-
// the row it also repainted whatever `trailing` renders — a
|
|
132
|
-
// `Tag`'s label is a span too, and it went invisible against
|
|
133
|
-
// its own fill.
|
|
134
|
-
isDisabled && 'bg-ods-bg',
|
|
135
|
-
itemClassName,
|
|
136
|
-
)}
|
|
137
|
-
>
|
|
108
|
+
const hasFooter = option.footer != null;
|
|
109
|
+
const row = (
|
|
110
|
+
<>
|
|
138
111
|
<RadioGroupPrimitive.Item
|
|
139
112
|
id={itemId}
|
|
140
113
|
value={option.value}
|
|
@@ -186,6 +159,48 @@ const RadioGroupBlock = forwardRef<ComponentRef<typeof RadioGroupPrimitive.Root>
|
|
|
186
159
|
)}
|
|
187
160
|
</div>
|
|
188
161
|
{option.trailing && <div className="ml-auto flex shrink-0 items-center">{option.trailing}</div>}
|
|
162
|
+
</>
|
|
163
|
+
);
|
|
164
|
+
return (
|
|
165
|
+
<label
|
|
166
|
+
key={option.value}
|
|
167
|
+
htmlFor={itemId}
|
|
168
|
+
aria-disabled={isDisabled || undefined}
|
|
169
|
+
className={cn(
|
|
170
|
+
'flex w-full items-center gap-3',
|
|
171
|
+
isHorizontal && 'min-w-0 flex-1',
|
|
172
|
+
'transition-colors duration-200',
|
|
173
|
+
isDisabled ? 'cursor-not-allowed' : 'cursor-pointer',
|
|
174
|
+
// Hover / active move the BACKGROUND only, like Input and
|
|
175
|
+
// CheckboxBlock — the border stays put so an unselected row
|
|
176
|
+
// never flashes "selected" under the cursor.
|
|
177
|
+
!isDisabled && 'hover:bg-ods-bg-hover active:bg-ods-bg-active',
|
|
178
|
+
isGrouped
|
|
179
|
+
? cn(
|
|
180
|
+
'bg-ods-card px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs,8px)]',
|
|
181
|
+
!isLast && (isHorizontal ? 'border-r' : 'border-b'),
|
|
182
|
+
error ? 'border-ods-error' : 'border-ods-border',
|
|
183
|
+
)
|
|
184
|
+
: cn(
|
|
185
|
+
'rounded-[6px] border bg-ods-card p-[var(--spacing-system-sf)]',
|
|
186
|
+
!isDisabled &&
|
|
187
|
+
'has-[[data-state=checked]]:bg-[var(--ods-system-greys-soft-grey)]/30 has-[[data-state=checked]]:border-ods-accent/40',
|
|
188
|
+
error ? 'border-ods-error' : 'border-ods-border',
|
|
189
|
+
),
|
|
190
|
+
// Disabled matches a disabled field: flat `ods-bg` fill and
|
|
191
|
+
// greyed text, not a fade of the whole row. The greying lives
|
|
192
|
+
// on the TEXT COLUMN below, not here: as a descendant rule on
|
|
193
|
+
// the row it also repainted whatever `trailing` renders — a
|
|
194
|
+
// `Tag`'s label is a span too, and it went invisible against
|
|
195
|
+
// its own fill.
|
|
196
|
+
isDisabled && 'bg-ods-bg',
|
|
197
|
+
// With a footer the option stacks: its row, then the footer.
|
|
198
|
+
hasFooter && 'flex-col items-stretch gap-[var(--spacing-system-sf)]',
|
|
199
|
+
itemClassName,
|
|
200
|
+
)}
|
|
201
|
+
>
|
|
202
|
+
{hasFooter ? <div className="flex w-full items-center gap-[var(--spacing-system-sf)]">{row}</div> : row}
|
|
203
|
+
{option.footer}
|
|
189
204
|
</label>
|
|
190
205
|
);
|
|
191
206
|
})}
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
import type React from 'react';
|
|
4
4
|
import {
|
|
5
|
+
createContext,
|
|
6
|
+
useContext,
|
|
5
7
|
useState,
|
|
6
8
|
useEffect,
|
|
7
9
|
useLayoutEffect,
|
|
@@ -71,6 +73,31 @@ export interface TabItem {
|
|
|
71
73
|
accent?: string;
|
|
72
74
|
}
|
|
73
75
|
|
|
76
|
+
/**
|
|
77
|
+
* How a strip marks its tabs.
|
|
78
|
+
*
|
|
79
|
+
* - `default`: the tabs touch, and the open tab is marked by the underline alone.
|
|
80
|
+
* - `admin`: the product admin's tab bar. A gap between tabs, and the open and the
|
|
81
|
+
* hovered tab carry an accent fade above the underline.
|
|
82
|
+
*/
|
|
83
|
+
export type TabNavigationVariant = 'default' | 'admin';
|
|
84
|
+
|
|
85
|
+
const TabNavigationVariantContext = createContext<TabNavigationVariant>('default');
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Sets the variant of every `TabNavigation` below it that does not pass its own
|
|
89
|
+
* `variant`. An app whose tab bars all share one look mounts it once in its layout.
|
|
90
|
+
*/
|
|
91
|
+
export function TabNavigationVariantProvider({
|
|
92
|
+
variant,
|
|
93
|
+
children,
|
|
94
|
+
}: {
|
|
95
|
+
variant: TabNavigationVariant;
|
|
96
|
+
children: React.ReactNode;
|
|
97
|
+
}) {
|
|
98
|
+
return <TabNavigationVariantContext.Provider value={variant}>{children}</TabNavigationVariantContext.Provider>;
|
|
99
|
+
}
|
|
100
|
+
|
|
74
101
|
export interface TabNavigationUrlSyncOptions {
|
|
75
102
|
paramName?: string; // Default: 'tab'
|
|
76
103
|
replaceState?: boolean; // Default: true (use replace instead of push)
|
|
@@ -99,6 +126,8 @@ interface TabNavigationProps {
|
|
|
99
126
|
tabAlign?: 'center' | 'start';
|
|
100
127
|
/** How long the underline takes to reach a new `TabItem.progress` value. Default 200. */
|
|
101
128
|
progressTransitionMs?: number;
|
|
129
|
+
/** How the strip marks its tabs. Omitted: the nearest `TabNavigationVariantProvider`'s, else `default`. */
|
|
130
|
+
variant?: TabNavigationVariant;
|
|
102
131
|
|
|
103
132
|
// URL sync mode
|
|
104
133
|
urlSync?: boolean | TabNavigationUrlSyncOptions;
|
|
@@ -145,6 +174,7 @@ interface TabBarProps {
|
|
|
145
174
|
stretchTabs: boolean;
|
|
146
175
|
tabAlign: 'center' | 'start';
|
|
147
176
|
progressTransitionMs: number;
|
|
177
|
+
variant: TabNavigationVariant;
|
|
148
178
|
}
|
|
149
179
|
|
|
150
180
|
/**
|
|
@@ -170,7 +200,9 @@ const TabBar = memo(function TabBarImpl({
|
|
|
170
200
|
stretchTabs,
|
|
171
201
|
tabAlign,
|
|
172
202
|
progressTransitionMs,
|
|
203
|
+
variant,
|
|
173
204
|
}: TabBarProps) {
|
|
205
|
+
const isAdmin = variant === 'admin';
|
|
174
206
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
175
207
|
const activeTabRef = useRef<HTMLButtonElement>(null);
|
|
176
208
|
const isFirstActiveScrollRef = useRef(true);
|
|
@@ -396,7 +428,10 @@ const TabBar = memo(function TabBarImpl({
|
|
|
396
428
|
`relative` makes this the offsetParent the underline is measured against. */}
|
|
397
429
|
<div
|
|
398
430
|
ref={scrollRef}
|
|
399
|
-
className=
|
|
431
|
+
className={cn(
|
|
432
|
+
'scrollbar-hide relative flex h-full items-center justify-start overflow-x-auto overflow-y-hidden',
|
|
433
|
+
isAdmin && 'gap-[var(--spacing-system-xxs)]',
|
|
434
|
+
)}
|
|
400
435
|
>
|
|
401
436
|
{tabs.map(tab => {
|
|
402
437
|
const isActive = activeTab === tab.id;
|
|
@@ -417,11 +452,19 @@ const TabBar = memo(function TabBarImpl({
|
|
|
417
452
|
// `outline-none` above drops the browser's own focus ring, so
|
|
418
453
|
// put one back or the strip is un-navigable by keyboard — you
|
|
419
454
|
// can tab through it with nothing to show where you are. Inset,
|
|
420
|
-
// so it stays within the button
|
|
455
|
+
// so it stays within the button and never overlaps a neighbour.
|
|
421
456
|
'focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-ods-focus',
|
|
422
457
|
stretchTabs && 'flex-1',
|
|
423
458
|
tab.avatar && 'gap-[var(--spacing-system-xsf)]',
|
|
424
|
-
//
|
|
459
|
+
// `default`: the open tab is marked by the underline alone, no surface
|
|
460
|
+
// of its own. `admin`: the open and the hovered tab carry an accent
|
|
461
|
+
// fade. ODS colour vars hold hex values, so Tailwind alpha modifiers
|
|
462
|
+
// (to-ods-accent/10) silently produce no CSS; color-mix() accepts
|
|
463
|
+
// hex, so the 10%-alpha stop is derived from the token.
|
|
464
|
+
isAdmin &&
|
|
465
|
+
(isActive
|
|
466
|
+
? 'bg-gradient-to-b from-transparent to-[color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)]'
|
|
467
|
+
: 'hover:bg-gradient-to-b hover:from-transparent hover:to-[color-mix(in_srgb,var(--color-accent-primary)_10%,transparent)]'),
|
|
425
468
|
)}
|
|
426
469
|
>
|
|
427
470
|
{tab.avatar ? (
|
|
@@ -436,7 +479,7 @@ const TabBar = memo(function TabBarImpl({
|
|
|
436
479
|
'h-4 w-4 transition-colors content-md:h-6 content-md:w-6',
|
|
437
480
|
isActive
|
|
438
481
|
? !tab.accent && 'text-ods-accent'
|
|
439
|
-
: 'text-ods-text-secondary group-hover:text-ods-text-primary',
|
|
482
|
+
: cn('text-ods-text-secondary', !isAdmin && 'group-hover:text-ods-text-primary'),
|
|
440
483
|
)}
|
|
441
484
|
style={isActive && tab.accent ? { color: tab.accent } : undefined}
|
|
442
485
|
/>
|
|
@@ -453,7 +496,9 @@ const TabBar = memo(function TabBarImpl({
|
|
|
453
496
|
<span
|
|
454
497
|
className={cn(
|
|
455
498
|
'whitespace-nowrap transition-colors text-h4',
|
|
456
|
-
isActive
|
|
499
|
+
isActive
|
|
500
|
+
? 'text-ods-text-primary'
|
|
501
|
+
: cn('text-ods-text-secondary', !isAdmin && 'group-hover:text-ods-text-primary'),
|
|
457
502
|
)}
|
|
458
503
|
>
|
|
459
504
|
{tab.label}
|
|
@@ -542,10 +587,13 @@ export function TabNavigation({
|
|
|
542
587
|
stretchTabs = false,
|
|
543
588
|
tabAlign = 'center',
|
|
544
589
|
progressTransitionMs = 200,
|
|
590
|
+
variant: variantProp,
|
|
545
591
|
urlSync = false,
|
|
546
592
|
defaultTab,
|
|
547
593
|
children,
|
|
548
594
|
}: TabNavigationProps) {
|
|
595
|
+
const providedVariant = useContext(TabNavigationVariantContext);
|
|
596
|
+
const variant = variantProp ?? providedVariant;
|
|
549
597
|
const router = useRouter();
|
|
550
598
|
const pathname = usePathname();
|
|
551
599
|
const searchParams = useSearchParams();
|
|
@@ -717,6 +765,7 @@ export function TabNavigation({
|
|
|
717
765
|
stretchTabs={stretchTabs}
|
|
718
766
|
tabAlign={tabAlign}
|
|
719
767
|
progressTransitionMs={progressTransitionMs}
|
|
768
|
+
variant={variant}
|
|
720
769
|
/>
|
|
721
770
|
|
|
722
771
|
{/* Render children with active tab if provided */}
|
|
@@ -86,6 +86,34 @@ export interface EmbedAuthAdapter {
|
|
|
86
86
|
* host's own auth layer, not to this fetch wrapper.
|
|
87
87
|
*/
|
|
88
88
|
refresh?: () => Promise<boolean>;
|
|
89
|
+
/**
|
|
90
|
+
* Optional guard, run before every request goes out — the first attempt
|
|
91
|
+
* and the retry after a `refresh`. Throw — or reject — to refuse the
|
|
92
|
+
* request: `embedAuthedFetch` rejects with that error and nothing reaches
|
|
93
|
+
* the network.
|
|
94
|
+
*
|
|
95
|
+
* For a host-wide state that makes every request pointless. openframe's
|
|
96
|
+
* mobile force-update is the case it exists for: once the gateway has
|
|
97
|
+
* refused this client build, every later request is refused locally, so no
|
|
98
|
+
* surface's retry or reconnect loop keeps hitting the gateway. Runs inside
|
|
99
|
+
* the returned promise (after the synchronous same-origin guard), so
|
|
100
|
+
* callers see a rejection, never a synchronous throw.
|
|
101
|
+
*/
|
|
102
|
+
beforeRequest?: () => void | Promise<void>;
|
|
103
|
+
/**
|
|
104
|
+
* Optional response hook, run on every response (the post-refresh retry's
|
|
105
|
+
* too) BEFORE the 401 self-heal. Throw — or reject — to fail the request
|
|
106
|
+
* with that error: `embedAuthedFetch` rejects with it, and neither
|
|
107
|
+
* `refresh` nor a retry runs. Return to let the response continue through
|
|
108
|
+
* the wrapper unchanged.
|
|
109
|
+
*
|
|
110
|
+
* For a status the host handles app-wide rather than per surface — e.g.
|
|
111
|
+
* openframe's gateway answering `426 Upgrade Required` to a too-old mobile
|
|
112
|
+
* build, which swaps the whole app for an "update required" screen and
|
|
113
|
+
* must never be retried or read as an auth failure. Read the body from
|
|
114
|
+
* `response.clone()`: once the hook returns, the caller reads the original.
|
|
115
|
+
*/
|
|
116
|
+
onResponse?: (response: Response) => void | Promise<void>;
|
|
89
117
|
/**
|
|
90
118
|
* Exact extra origins (`scheme://host[:port]`, as produced by `URL.origin`)
|
|
91
119
|
* the same-origin guard additionally accepts. The guard normally rejects
|
|
@@ -253,6 +281,11 @@ export function needsBearerAssetFetch(url: string): boolean {
|
|
|
253
281
|
* no longer need a host-side `window.fetch` monkey-patch to survive an
|
|
254
282
|
* expired access token mid-chat. With no adapter (or no `refresh`), the
|
|
255
283
|
* 401 passes straight through unchanged.
|
|
284
|
+
*
|
|
285
|
+
* **Host hooks:** an adapter's `beforeRequest` may refuse a request before it
|
|
286
|
+
* is sent, and its `onResponse` sees every response ahead of the 401
|
|
287
|
+
* self-heal and may fail the request instead (see their docs). Without them
|
|
288
|
+
* the wrapper behaves exactly as described above.
|
|
256
289
|
*/
|
|
257
290
|
export function embedAuthedFetch(url: string, init: RequestInit = {}): Promise<Response> {
|
|
258
291
|
// Same-origin guard runs SYNCHRONOUSLY (not awaited inside the async
|
|
@@ -354,6 +387,11 @@ async function fetchWithRefresh(
|
|
|
354
387
|
baseHeaders: Record<string, string>,
|
|
355
388
|
isRetry: boolean,
|
|
356
389
|
): Promise<Response> {
|
|
390
|
+
const adapter = getRegisteredAuthAdapter();
|
|
391
|
+
// Per attempt, so a retry after `refresh` is refused too when the host's
|
|
392
|
+
// state changed while the refresh was in flight.
|
|
393
|
+
await adapter?.beforeRequest?.();
|
|
394
|
+
|
|
357
395
|
// Re-run the merge each attempt: `applyProxyAuth` reads the latest stored
|
|
358
396
|
// proxy creds and `getHeaders()` reads the latest bearer, so a retry after
|
|
359
397
|
// refresh carries the rotated token rather than the stale one. `{...baseHeaders}`
|
|
@@ -365,7 +403,6 @@ async function fetchWithRefresh(
|
|
|
365
403
|
// is the host's explicit "this is my auth model" override, intentionally
|
|
366
404
|
// last-writer-wins. When no adapter is registered, this is a zero-cost
|
|
367
405
|
// no-op (object spread of `{}`).
|
|
368
|
-
const adapter = getRegisteredAuthAdapter();
|
|
369
406
|
if (adapter?.getHeaders) {
|
|
370
407
|
// Filter `undefined` values — the adapter type allows them so consumers
|
|
371
408
|
// don't have to narrow `{ Authorization: token ? '…' : undefined }`-shaped
|
|
@@ -387,6 +424,10 @@ async function fetchWithRefresh(
|
|
|
387
424
|
credentials,
|
|
388
425
|
});
|
|
389
426
|
|
|
427
|
+
// Ahead of the 401 self-heal: a status the host claims by throwing must
|
|
428
|
+
// never reach `refresh` or a retry.
|
|
429
|
+
await adapter?.onResponse?.(response);
|
|
430
|
+
|
|
390
431
|
// 401 self-heal: refresh the token once and retry. Only when an adapter
|
|
391
432
|
// opted into `refresh`, and only on the first attempt — a 401 on the
|
|
392
433
|
// retry means the fresh token is also unauthorized, so surface it.
|