@flamingo-stack/openframe-frontend-core 0.0.490-snapshot.20260728181420 → 0.0.490

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (154) hide show
  1. package/dist/{chunk-VD4QDSP7.cjs → chunk-2MKUHJ3O.cjs} +40 -40
  2. package/dist/{chunk-VD4QDSP7.cjs.map → chunk-2MKUHJ3O.cjs.map} +1 -1
  3. package/dist/{chunk-EHHX22JL.cjs → chunk-3IEVA5AO.cjs} +93 -93
  4. package/dist/{chunk-EHHX22JL.cjs.map → chunk-3IEVA5AO.cjs.map} +1 -1
  5. package/dist/{chunk-IUGJAILN.js → chunk-6CFFISUC.js} +3 -3
  6. package/dist/{chunk-CZTKQVIQ.js → chunk-6CJ2K7BD.js} +3 -3
  7. package/dist/{chunk-ZEC5U5T7.js → chunk-6QK3ONDD.js} +2 -2
  8. package/dist/{chunk-GL33BHGZ.cjs → chunk-7I42V2OD.cjs} +73 -73
  9. package/dist/{chunk-GL33BHGZ.cjs.map → chunk-7I42V2OD.cjs.map} +1 -1
  10. package/dist/{chunk-GTMW4KY7.js → chunk-AIRNG27G.js} +6 -6
  11. package/dist/{chunk-ITV6LSG7.cjs → chunk-AW63ET2A.cjs} +19 -19
  12. package/dist/{chunk-ITV6LSG7.cjs.map → chunk-AW63ET2A.cjs.map} +1 -1
  13. package/dist/{chunk-TTYJAZ7P.cjs → chunk-BIKOJ4FH.cjs} +32 -58
  14. package/dist/chunk-BIKOJ4FH.cjs.map +1 -0
  15. package/dist/{chunk-35CTCUJ7.js → chunk-BZMOEWWS.js} +6 -6
  16. package/dist/{chunk-QNFAZIFC.cjs → chunk-D5LQMRPU.cjs} +5 -5
  17. package/dist/{chunk-QNFAZIFC.cjs.map → chunk-D5LQMRPU.cjs.map} +1 -1
  18. package/dist/{chunk-T4BUS6JV.js → chunk-EGXCWZYP.js} +3 -3
  19. package/dist/{chunk-HRMOCJKU.cjs → chunk-EJQUYZXF.cjs} +12 -12
  20. package/dist/{chunk-HRMOCJKU.cjs.map → chunk-EJQUYZXF.cjs.map} +1 -1
  21. package/dist/{chunk-J2V4B6FF.cjs → chunk-FY5XAW3H.cjs} +18 -18
  22. package/dist/{chunk-J2V4B6FF.cjs.map → chunk-FY5XAW3H.cjs.map} +1 -1
  23. package/dist/{chunk-YKE2KZ2Y.cjs → chunk-GMPYR3MX.cjs} +33 -33
  24. package/dist/{chunk-YKE2KZ2Y.cjs.map → chunk-GMPYR3MX.cjs.map} +1 -1
  25. package/dist/{chunk-6O3YTVLG.cjs → chunk-GUTYUMGV.cjs} +12 -12
  26. package/dist/{chunk-6O3YTVLG.cjs.map → chunk-GUTYUMGV.cjs.map} +1 -1
  27. package/dist/{chunk-QKWOBV55.js → chunk-GZK7FWJJ.js} +12 -12
  28. package/dist/chunk-GZK7FWJJ.js.map +1 -0
  29. package/dist/{chunk-7NRRONK5.cjs → chunk-IEZ3YBAP.cjs} +32 -32
  30. package/dist/{chunk-7NRRONK5.cjs.map → chunk-IEZ3YBAP.cjs.map} +1 -1
  31. package/dist/{chunk-THC3K262.cjs → chunk-KP5KRRD6.cjs} +3 -3
  32. package/dist/{chunk-THC3K262.cjs.map → chunk-KP5KRRD6.cjs.map} +1 -1
  33. package/dist/{chunk-U2JIGUXF.js → chunk-MPYDX5WX.js} +4 -4
  34. package/dist/{chunk-ZDUJ6BDI.js → chunk-NGEFYPY7.js} +4 -4
  35. package/dist/{chunk-CYIUPI7J.js → chunk-OQIMH2WQ.js} +1004 -1167
  36. package/dist/{chunk-CYIUPI7J.js.map → chunk-OQIMH2WQ.js.map} +1 -1
  37. package/dist/{chunk-WEUNPE3S.js → chunk-PB3BTZDU.js} +113 -139
  38. package/dist/chunk-PB3BTZDU.js.map +1 -0
  39. package/dist/{chunk-6WTCJI5R.js → chunk-RW2LPL72.js} +2 -2
  40. package/dist/{chunk-SGL6URIJ.cjs → chunk-STYA77PH.cjs} +12 -12
  41. package/dist/chunk-STYA77PH.cjs.map +1 -0
  42. package/dist/{chunk-PVSXTCJY.cjs → chunk-UZZT65PQ.cjs} +40 -40
  43. package/dist/{chunk-PVSXTCJY.cjs.map → chunk-UZZT65PQ.cjs.map} +1 -1
  44. package/dist/{chunk-2YQK6LHR.js → chunk-VDZO6IDR.js} +5 -5
  45. package/dist/{chunk-QF54RPHZ.js → chunk-VL2YMDPH.js} +6 -6
  46. package/dist/{chunk-2RTNC3KP.cjs → chunk-VUXNXWOQ.cjs} +873 -1036
  47. package/dist/chunk-VUXNXWOQ.cjs.map +1 -0
  48. package/dist/{chunk-6XNWT2KC.js → chunk-WLM3F3WU.js} +3 -3
  49. package/dist/components/case-studies/index.cjs +10 -10
  50. package/dist/components/case-studies/index.js +4 -4
  51. package/dist/components/chat/index.cjs +4 -4
  52. package/dist/components/chat/index.js +3 -3
  53. package/dist/components/contact/index.cjs +5 -5
  54. package/dist/components/contact/index.js +4 -4
  55. package/dist/components/docs/index.cjs +7 -7
  56. package/dist/components/docs/index.js +6 -6
  57. package/dist/components/embeds/index.cjs +5 -5
  58. package/dist/components/embeds/index.js +4 -4
  59. package/dist/components/faq/index.cjs +6 -6
  60. package/dist/components/faq/index.js +5 -5
  61. package/dist/components/features/index.cjs +4 -4
  62. package/dist/components/features/index.js +3 -3
  63. package/dist/components/features/select-button.d.ts.map +1 -1
  64. package/dist/components/help-center-pages/index.cjs +23 -23
  65. package/dist/components/help-center-pages/index.js +14 -14
  66. package/dist/components/index.cjs +161 -161
  67. package/dist/components/index.js +12 -12
  68. package/dist/components/layout/page-layout.d.ts +1 -7
  69. package/dist/components/layout/page-layout.d.ts.map +1 -1
  70. package/dist/components/layout/title-block.d.ts +1 -23
  71. package/dist/components/layout/title-block.d.ts.map +1 -1
  72. package/dist/components/navigation/index.cjs +4 -4
  73. package/dist/components/navigation/index.js +3 -3
  74. package/dist/components/onboarding-guides/index.cjs +8 -8
  75. package/dist/components/onboarding-guides/index.js +7 -7
  76. package/dist/components/related-content/index.cjs +6 -6
  77. package/dist/components/related-content/index.js +5 -5
  78. package/dist/components/tickets/index.cjs +7 -7
  79. package/dist/components/tickets/index.js +6 -6
  80. package/dist/components/ui/autocomplete.d.ts.map +1 -1
  81. package/dist/components/ui/checkbox-block.d.ts.map +1 -1
  82. package/dist/components/ui/date-picker.d.ts.map +1 -1
  83. package/dist/components/ui/field-wrapper.d.ts +1 -10
  84. package/dist/components/ui/field-wrapper.d.ts.map +1 -1
  85. package/dist/components/ui/file-manager/index.cjs +50 -50
  86. package/dist/components/ui/file-manager/index.js +1 -1
  87. package/dist/components/ui/floating-tooltip.d.ts +1 -8
  88. package/dist/components/ui/floating-tooltip.d.ts.map +1 -1
  89. package/dist/components/ui/index.cjs +4 -4
  90. package/dist/components/ui/index.js +3 -3
  91. package/dist/components/ui/input-trigger.d.ts.map +1 -1
  92. package/dist/components/ui/input.d.ts.map +1 -1
  93. package/dist/components/ui/page-actions.d.ts +1 -7
  94. package/dist/components/ui/page-actions.d.ts.map +1 -1
  95. package/dist/components/ui/radio-group.d.ts.map +1 -1
  96. package/dist/components/ui/search-input.d.ts.map +1 -1
  97. package/dist/components/ui/select.d.ts.map +1 -1
  98. package/dist/components/ui/tab-navigation.d.ts +1 -10
  99. package/dist/components/ui/tab-navigation.d.ts.map +1 -1
  100. package/dist/components/ui/tag-search-input.d.ts.map +1 -1
  101. package/dist/components/ui/textarea.d.ts.map +1 -1
  102. package/dist/components/ui/truncate-text.d.ts +2 -2
  103. package/dist/components/ui/truncate-text.d.ts.map +1 -1
  104. package/dist/hooks/index.cjs +2 -4
  105. package/dist/hooks/index.cjs.map +1 -1
  106. package/dist/hooks/index.js +1 -3
  107. package/dist/hooks/ui/index.d.ts +0 -1
  108. package/dist/hooks/ui/index.d.ts.map +1 -1
  109. package/dist/index.cjs +4 -6
  110. package/dist/index.cjs.map +1 -1
  111. package/dist/index.js +3 -5
  112. package/package.json +1 -1
  113. package/src/components/features/select-button.tsx +1 -4
  114. package/src/components/layout/page-layout.tsx +0 -16
  115. package/src/components/layout/title-block.tsx +7 -106
  116. package/src/components/ui/.field-wrapper.md +4 -5
  117. package/src/components/ui/.tab-navigation.md +1 -14
  118. package/src/components/ui/autocomplete.tsx +1 -13
  119. package/src/components/ui/button/button.tsx +1 -1
  120. package/src/components/ui/checkbox-block.tsx +3 -27
  121. package/src/components/ui/date-picker.tsx +1 -15
  122. package/src/components/ui/field-wrapper.tsx +6 -12
  123. package/src/components/ui/floating-tooltip.tsx +1 -9
  124. package/src/components/ui/input-trigger.tsx +1 -8
  125. package/src/components/ui/input.tsx +1 -8
  126. package/src/components/ui/page-actions.tsx +30 -142
  127. package/src/components/ui/radio-group.tsx +3 -28
  128. package/src/components/ui/search-input.tsx +1 -2
  129. package/src/components/ui/select.tsx +0 -7
  130. package/src/components/ui/tab-navigation.tsx +4 -26
  131. package/src/components/ui/tag-search-input.tsx +2 -9
  132. package/src/components/ui/textarea.tsx +1 -8
  133. package/src/components/ui/truncate-text.tsx +18 -3
  134. package/src/hooks/ui/index.ts +0 -1
  135. package/dist/chunk-2RTNC3KP.cjs.map +0 -1
  136. package/dist/chunk-QKWOBV55.js.map +0 -1
  137. package/dist/chunk-SGL6URIJ.cjs.map +0 -1
  138. package/dist/chunk-TTYJAZ7P.cjs.map +0 -1
  139. package/dist/chunk-WEUNPE3S.js.map +0 -1
  140. package/dist/hooks/ui/use-is-truncated.d.ts +0 -33
  141. package/dist/hooks/ui/use-is-truncated.d.ts.map +0 -1
  142. package/src/hooks/ui/use-is-truncated.ts +0 -66
  143. /package/dist/{chunk-IUGJAILN.js.map → chunk-6CFFISUC.js.map} +0 -0
  144. /package/dist/{chunk-CZTKQVIQ.js.map → chunk-6CJ2K7BD.js.map} +0 -0
  145. /package/dist/{chunk-ZEC5U5T7.js.map → chunk-6QK3ONDD.js.map} +0 -0
  146. /package/dist/{chunk-GTMW4KY7.js.map → chunk-AIRNG27G.js.map} +0 -0
  147. /package/dist/{chunk-35CTCUJ7.js.map → chunk-BZMOEWWS.js.map} +0 -0
  148. /package/dist/{chunk-T4BUS6JV.js.map → chunk-EGXCWZYP.js.map} +0 -0
  149. /package/dist/{chunk-U2JIGUXF.js.map → chunk-MPYDX5WX.js.map} +0 -0
  150. /package/dist/{chunk-ZDUJ6BDI.js.map → chunk-NGEFYPY7.js.map} +0 -0
  151. /package/dist/{chunk-6WTCJI5R.js.map → chunk-RW2LPL72.js.map} +0 -0
  152. /package/dist/{chunk-2YQK6LHR.js.map → chunk-VDZO6IDR.js.map} +0 -0
  153. /package/dist/{chunk-QF54RPHZ.js.map → chunk-VL2YMDPH.js.map} +0 -0
  154. /package/dist/{chunk-6XNWT2KC.js.map → chunk-WLM3F3WU.js.map} +0 -0
@@ -26,13 +26,6 @@ interface FloatingTooltipProps {
26
26
  delayDuration?: number
27
27
  /** Disable the tooltip without unmounting the trigger wrapper. */
28
28
  disabled?: boolean
29
- /**
30
- * Classes for the trigger wrapper — the `div` this component puts around
31
- * `children` to anchor the tooltip. That div becomes the flex/grid item in the
32
- * caller's layout, so anything the child needed there (`min-w-0`, `flex-1`)
33
- * must move onto it. Omit it and the wrapper carries no class, exactly as before.
34
- */
35
- triggerClassName?: string
36
29
  }
37
30
 
38
31
  // Parse colored text markup like [YELLOW]text[/YELLOW] into JSX
@@ -92,7 +85,6 @@ export function FloatingTooltip({
92
85
  className,
93
86
  delayDuration = 0,
94
87
  disabled = false,
95
- triggerClassName,
96
88
  }: FloatingTooltipProps) {
97
89
  const [isOpen, setIsOpen] = React.useState(false)
98
90
  const arrowRef = React.useRef<HTMLDivElement>(null)
@@ -159,7 +151,7 @@ export function FloatingTooltip({
159
151
 
160
152
  return (
161
153
  <>
162
- <div ref={refs.setReference} className={triggerClassName} {...getReferenceProps()}>
154
+ <div ref={refs.setReference} {...getReferenceProps()}>
163
155
  {children}
164
156
  </div>
165
157
  <FloatingPortal>
@@ -52,14 +52,7 @@ export const InputTrigger = React.forwardRef<HTMLButtonElement, InputTriggerProp
52
52
  "data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent",
53
53
  invalid &&
54
54
  "border-ods-error enabled:hover:border-ods-error data-[state=open]:border-ods-error",
55
- // Disabled - match Input / SelectTrigger: grey the TEXT, don't fade the
56
- // whole control (a blanket opacity also washes out the border and the
57
- // adornment icons, which no other field does). The child rule catches
58
- // the label span, which sets its own placeholder colour. Scoped to
59
- // DIRECT children: `selectedLabel` is a ReactNode the caller owns, so
60
- // anything inside it that sets its own colour keeps it.
61
- "disabled:cursor-not-allowed disabled:bg-ods-bg",
62
- "disabled:text-ods-text-disabled disabled:[&>span]:text-ods-text-disabled disabled:[&_svg]:text-ods-text-disabled",
55
+ "disabled:cursor-not-allowed disabled:opacity-50 disabled:bg-ods-bg",
63
56
  "transition-colors duration-200",
64
57
  className,
65
58
  )}
@@ -83,15 +83,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
83
83
  "bg-ods-card border-ods-border has-[:focus]:border-ods-accent",
84
84
  // Hover & active (not disabled)
85
85
  !props.disabled && "hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-active",
86
- // Disabled. The adornments (and any icon inside them) carry their own
87
- // `text-ods-text-secondary`, so grey them from here — a disabled field
88
- // must read as one flat colour, not grey text next to a live icon.
89
- // Scoped to the DIRECT span children (the two adornment wrappers): a
90
- // descendant rule would also repaint whatever the caller renders INSIDE
91
- // an adornment, which is passed verbatim precisely so it can own its
92
- // colour (a `Tag` there went invisible against its own fill).
86
+ // Disabled
93
87
  props.disabled && "!cursor-not-allowed bg-ods-bg",
94
- "has-[:disabled]:[&>span]:text-ods-text-disabled has-[:disabled]:[&_svg]:text-ods-text-disabled",
95
88
  // Invalid
96
89
  isInvalid && invalidBorderClasses[variantIsInvalid ? (errorVariant as "error" | "warning") : "error"],
97
90
  className
@@ -6,7 +6,6 @@ import { Chevron02DownIcon } from '../icons-v2-generated'
6
6
  import { ActionsMenuDropdown, type ActionsMenuGroup, type ActionsMenuItem } from './actions-menu'
7
7
  import type { ButtonProps, SplitButtonIconAction } from './button'
8
8
  import { Button, SplitButton } from './button'
9
- import { Skeleton } from './skeleton'
10
9
  import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip'
11
10
 
12
11
  export type PageActionButton = {
@@ -207,104 +206,26 @@ export interface PageActionsProps {
207
206
  */
208
207
  selector?: React.ReactNode
209
208
  className?: string
210
- /**
211
- * Render placeholders instead of the actions — for pages whose action SET
212
- * depends on data still in flight. Opt-in: a page whose actions are already
213
- * final while its title loads keeps rendering them.
214
- */
215
- loading?: boolean
216
209
  }
217
210
 
218
211
  const ACTIONS_GAP = 'gap-[var(--spacing-system-xs)]'
219
212
 
220
- /** Matches `Button` size="icon" / size="default" exactly, so a placeholder and the
221
- * button that replaces it occupy the same box at both breakpoints. `rounded-md`
222
- * is the same token `Button` itself uses — a raw `6px` would agree today and
223
- * drift the moment a platform overrides `--radius`. */
224
- const SKELETON_HEIGHT = 'h-11 md:h-12'
225
- const SKELETON_ICON_WIDTH = 'w-11 md:w-12'
226
- const SKELETON_LABEL_WIDTH = 'w-[120px] md:w-[140px]'
227
- const SKELETON_RADIUS = 'rounded-md'
228
-
229
- /**
230
- * Placeholders for actions whose SHAPE isn't known yet — the page is still
231
- * loading the record that decides which actions exist (e.g. an archived entity
232
- * offers "Unarchive" where an active one offers "Edit").
233
- *
234
- * Why not render the actions anyway: a button is a promise. Painting the
235
- * optimistic set means a flash of the wrong header AND a live click target that
236
- * can act on the wrong assumption before the data lands. A grey box promises
237
- * nothing and can't be clicked.
238
- *
239
- * The placeholders are derived from the actions the caller passed — one per
240
- * action, each sized like the button that action WILL render — so the common
241
- * case (the data confirms that set) settles with no layout shift at all. Sizing
242
- * per action matters: `renderRawActionButton` collapses a label-less action (or
243
- * one flagged `iconOnlyOnDesktop`) to a square `size="icon"` button, and a
244
- * 140px-wide placeholder snapping to 48px is exactly the shift this is meant to
245
- * avoid.
246
- *
247
- * An empty list still yields one placeholder: `loading` means the action set is
248
- * unknown, not known-empty. Its shape comes from `emptyFallback`, because what
249
- * an empty header settles into differs per variant — `icon-buttons` and
250
- * `menu-primary` fall back to the square "…" overflow trigger, `primary-buttons`
251
- * has no overflow trigger at all and settles into a labelled button.
252
- */
253
- function ActionSkeletons({
254
- actions,
255
- fullWidth,
256
- emptyFallback = 'icon',
257
- }: {
258
- actions: PageActionButton[]
259
- fullWidth?: boolean
260
- emptyFallback?: 'icon' | 'label'
261
- }) {
262
- const items = actions.length > 0 ? actions : [null]
263
- return (
264
- <>
265
- {items.map((action, idx) => {
266
- // Mirrors `renderRawActionButton`'s `isIconOnly`. The mobile bottom bar
267
- // stretches only labelled buttons (`fullWidth: !!action.label`), so an
268
- // icon action keeps its square box there too.
269
- const isIconOnly = action ? !action.label || !!action.iconOnlyOnDesktop : emptyFallback === 'icon'
270
- return (
271
- <Skeleton
272
- // Static placeholder list — index IS the identity here.
273
- key={`action-skeleton-${idx}`}
274
- className={cn(
275
- SKELETON_HEIGHT,
276
- SKELETON_RADIUS,
277
- isIconOnly ? SKELETON_ICON_WIDTH : fullWidth ? 'flex-1' : SKELETON_LABEL_WIDTH,
278
- )}
279
- />
280
- )
281
- })}
282
- </>
283
- )
284
- }
285
-
286
- /** Mobile placeholder for the variants that collapse to ONE trigger (icon button or "…"). */
287
- function MobileTriggerSkeleton() {
288
- return <Skeleton className={cn(SKELETON_HEIGHT, SKELETON_ICON_WIDTH, SKELETON_RADIUS)} />
289
- }
290
-
291
213
  export function PageActions({
292
214
  variant = 'icon-buttons',
293
215
  actions,
294
216
  menuActions,
295
217
  selector,
296
218
  className,
297
- loading,
298
219
  }: PageActionsProps) {
299
220
  if (variant === 'icon-buttons') {
300
- return <IconButtonsVariant actions={actions} menuActions={menuActions} selector={selector} className={className} loading={loading} />
221
+ return <IconButtonsVariant actions={actions} menuActions={menuActions} selector={selector} className={className} />
301
222
  }
302
223
 
303
224
  if (variant === 'menu-primary') {
304
- return <MenuPrimaryVariant actions={actions} menuActions={menuActions || []} selector={selector} className={className} loading={loading} />
225
+ return <MenuPrimaryVariant actions={actions} menuActions={menuActions || []} selector={selector} className={className} />
305
226
  }
306
227
 
307
- return <PrimaryButtonsVariant actions={actions} className={className} loading={loading} />
228
+ return <PrimaryButtonsVariant actions={actions} className={className} />
308
229
  }
309
230
 
310
231
  function IconButtonsVariant({
@@ -312,13 +233,11 @@ function IconButtonsVariant({
312
233
  menuActions,
313
234
  selector,
314
235
  className,
315
- loading,
316
236
  }: {
317
237
  actions: PageActionButton[]
318
238
  menuActions?: ActionsMenuGroup[]
319
239
  selector?: React.ReactNode
320
240
  className?: string
321
- loading?: boolean
322
241
  }) {
323
242
  const desktopActions = actions.filter(a => !a.showOnlyMobile)
324
243
  const hasMenuActions = !!menuActions && menuActions.some(g => g.items.length > 0)
@@ -338,25 +257,17 @@ function IconButtonsVariant({
338
257
  {/* Desktop: every action as an icon button + optional overflow menu */}
339
258
  <div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>
340
259
  {selector}
341
- {loading ? (
342
- <ActionSkeletons actions={desktopActions} />
343
- ) : (
344
- <>
345
- {desktopActions.map((action, idx) => (
346
- <React.Fragment key={actionKey(action, idx)}>
347
- {renderActionButton(action)}
348
- </React.Fragment>
349
- ))}
350
- {hasMenuActions && <ActionsMenuDropdown groups={menuActions} />}
351
- </>
352
- )}
260
+ {desktopActions.map((action, idx) => (
261
+ <React.Fragment key={actionKey(action, idx)}>
262
+ {renderActionButton(action)}
263
+ </React.Fragment>
264
+ ))}
265
+ {hasMenuActions && <ActionsMenuDropdown groups={menuActions} />}
353
266
  </div>
354
267
 
355
268
  {/* Mobile: single icon button OR all actions merged into one "..." menu */}
356
269
  <div className={cn('flex md:hidden', className)}>
357
- {loading ? (
358
- <MobileTriggerSkeleton />
359
- ) : useSingleActionMobile && singleAction ? (
270
+ {useSingleActionMobile && singleAction ? (
360
271
  renderActionButton(singleAction, { iconOnly: true })
361
272
  ) : hasMobileMenuItems ? (
362
273
  <ActionsMenuDropdown groups={mobileMenuGroups} />
@@ -373,11 +284,9 @@ function IconButtonsVariant({
373
284
  function PrimaryButtonsVariant({
374
285
  actions,
375
286
  className,
376
- loading,
377
287
  }: {
378
288
  actions: PageActionButton[]
379
289
  className?: string
380
- loading?: boolean
381
290
  }) {
382
291
  // Sort: outline first, accent last (rightmost on desktop).
383
292
  const sortedActions = [...actions].sort((a, b) => {
@@ -391,20 +300,14 @@ function PrimaryButtonsVariant({
391
300
  return (
392
301
  <>
393
302
  <div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>
394
- {loading ? (
395
- // No overflow trigger in this variant — an unknown action set settles
396
- // into a labelled button, not a square one.
397
- <ActionSkeletons actions={desktopActions} emptyFallback="label" />
398
- ) : (
399
- desktopActions.map((action, idx) => (
400
- <React.Fragment key={`desktop-${actionKey(action, idx)}`}>
401
- {renderActionButton(action)}
402
- </React.Fragment>
403
- ))
404
- )}
303
+ {desktopActions.map((action, idx) => (
304
+ <React.Fragment key={`desktop-${actionKey(action, idx)}`}>
305
+ {renderActionButton(action)}
306
+ </React.Fragment>
307
+ ))}
405
308
  </div>
406
309
 
407
- <MobileBottomActions actions={sortedActions} loading={loading} />
310
+ <MobileBottomActions actions={sortedActions} />
408
311
  </>
409
312
  )
410
313
  }
@@ -418,13 +321,11 @@ function MenuPrimaryVariant({
418
321
  menuActions,
419
322
  selector,
420
323
  className,
421
- loading,
422
324
  }: {
423
325
  actions: PageActionButton[]
424
326
  menuActions: ActionsMenuGroup[]
425
327
  selector?: React.ReactNode
426
328
  className?: string
427
- loading?: boolean
428
329
  }) {
429
330
  const desktopActions = actions.filter(a => !a.showOnlyMobile)
430
331
  const hasMenuActions = menuActions.some(g => g.items.length > 0)
@@ -439,47 +340,34 @@ function MenuPrimaryVariant({
439
340
  <>
440
341
  <div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>
441
342
  {selector}
442
- {loading ? (
443
- <ActionSkeletons actions={desktopActions} />
444
- ) : (
445
- <>
446
- {hasMenuActions && <ActionsMenuDropdown groups={menuActions} />}
447
- {desktopActions.map((action, idx) => (
448
- <React.Fragment key={`desktop-${actionKey(action, idx)}`}>
449
- {renderActionButton({ ...action, variant: action.variant || 'accent' })}
450
- </React.Fragment>
451
- ))}
452
- </>
453
- )}
343
+ {hasMenuActions && <ActionsMenuDropdown groups={menuActions} />}
344
+ {desktopActions.map((action, idx) => (
345
+ <React.Fragment key={`desktop-${actionKey(action, idx)}`}>
346
+ {renderActionButton({ ...action, variant: action.variant || 'accent' })}
347
+ </React.Fragment>
348
+ ))}
454
349
  </div>
455
350
 
456
351
  <div className={cn('flex md:hidden', className)}>
457
- {loading ? <MobileTriggerSkeleton /> : hasMobileMenuItems ? <ActionsMenuDropdown groups={mobileMenuGroups} /> : null}
352
+ {hasMobileMenuItems ? <ActionsMenuDropdown groups={mobileMenuGroups} /> : null}
458
353
  </div>
459
354
  </>
460
355
  )
461
356
  }
462
357
 
463
- function MobileBottomActions({ actions, loading }: { actions: PageActionButton[]; loading?: boolean }) {
358
+ function MobileBottomActions({ actions }: { actions: PageActionButton[] }) {
464
359
  return (
465
360
  <div className={cn(
466
361
  'fixed md:hidden bottom-0 left-0 right-0 z-50',
467
362
  'bg-ods-card border-t border-ods-border',
468
- // `lf` (24px at every breakpoint), NOT `l` — this bar is `md:hidden`, so the
469
- // only value it ever uses is the MOBILE one, and `l` is 16px there. The
470
- // fixed variant is what keeps the 24px the bar has always had.
471
- 'flex items-start p-[var(--spacing-system-lf)]',
363
+ 'flex items-start pt-6 pb-6 px-6',
472
364
  ACTIONS_GAP,
473
365
  )}>
474
- {loading ? (
475
- <ActionSkeletons actions={actions} fullWidth emptyFallback="label" />
476
- ) : (
477
- actions.map((action, idx) => (
478
- <React.Fragment key={`mobile-${actionKey(action, idx)}`}>
479
- {renderActionButton(action, { fullWidth: !!action.label })}
480
- </React.Fragment>
481
- ))
482
- )}
366
+ {actions.map((action, idx) => (
367
+ <React.Fragment key={`mobile-${actionKey(action, idx)}`}>
368
+ {renderActionButton(action, { fullWidth: !!action.label })}
369
+ </React.Fragment>
370
+ ))}
483
371
  </div>
484
372
  )
485
373
  }
@@ -81,10 +81,6 @@ 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}
88
84
  className={cn(
89
85
  "w-full",
90
86
  isGrouped
@@ -110,10 +106,6 @@ const RadioGroupBlock = React.forwardRef<
110
106
  "flex w-full items-center gap-3",
111
107
  "transition-colors duration-200",
112
108
  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",
117
109
  isGrouped
118
110
  ? cn(
119
111
  "bg-ods-card px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs,8px)]",
@@ -122,17 +114,12 @@ const RadioGroupBlock = React.forwardRef<
122
114
  )
123
115
  : cn(
124
116
  "rounded-[6px] border bg-ods-card p-[var(--spacing-system-sf)]",
117
+ !isDisabled && "hover:border-ods-accent/30",
125
118
  !isDisabled &&
126
119
  "has-[[data-state=checked]]:bg-[var(--ods-system-greys-soft-grey)]/30 has-[[data-state=checked]]:border-ods-accent/40",
127
120
  error ? "border-ods-error" : "border-ods-border"
128
121
  ),
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",
122
+ isDisabled && "opacity-50",
136
123
  itemClassName
137
124
  )}
138
125
  >
@@ -145,10 +132,7 @@ const RadioGroupBlock = React.forwardRef<
145
132
  error ? "border-ods-error" : "border-[var(--color-border-strong)]",
146
133
  "transition-colors duration-150",
147
134
  "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.
150
135
  "disabled:cursor-not-allowed",
151
- "disabled:data-[state=unchecked]:bg-ods-bg disabled:data-[state=unchecked]:border-ods-border",
152
136
  "data-[state=checked]:border-ods-accent"
153
137
  )}
154
138
  >
@@ -156,16 +140,7 @@ const RadioGroupBlock = React.forwardRef<
156
140
  <span className="block h-1/2 w-1/2 rounded-full bg-ods-accent" />
157
141
  </RadioGroupPrimitive.Indicator>
158
142
  </RadioGroupPrimitive.Item>
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
- >
143
+ <div className="flex min-w-0 flex-1 flex-col justify-center">
169
144
  <span
170
145
  className={cn(
171
146
  "font-[family-name:var(--font-h4-family)] font-[number:var(--font-h4-weight)] text-[length:var(--font-size-h4-body)] leading-[24px]",
@@ -106,8 +106,7 @@ 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
- // 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",
109
+ "disabled:cursor-not-allowed",
111
110
  "touch-manipulation"
112
111
  )
113
112
 
@@ -42,14 +42,7 @@ 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.
49
45
  "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",
53
46
  "transition-colors duration-200 cursor-pointer",
54
47
  "[&>span]:line-clamp-1",
55
48
  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, useDeferredValue, useMemo, useRef, useCallback } from 'react'
3
+ import React, { useState, useEffect, 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,14 +38,8 @@ 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
- * 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
41
+ // Render prop to provide active tab to children
42
+ children?: (activeTab: string) => React.ReactNode
49
43
  }
50
44
 
51
45
  export function TabNavigation({
@@ -125,22 +119,6 @@ export function TabNavigation({
125
119
  }
126
120
  }
127
121
 
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
-
144
122
  const scrollRef = useRef<HTMLDivElement>(null)
145
123
  const activeTabRef = useRef<HTMLButtonElement>(null)
146
124
  const isFirstActiveScrollRef = useRef(true)
@@ -313,7 +291,7 @@ export function TabNavigation({
313
291
  </div>
314
292
 
315
293
  {/* Render children with active tab if provided */}
316
- {children && children(deferredActiveTab, { isStale })}
294
+ {children && children(activeTab)}
317
295
  </>
318
296
  )
319
297
  }
@@ -140,13 +140,7 @@ 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",
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",
143
+ disabled && "cursor-not-allowed opacity-50",
150
144
  className,
151
145
  )}
152
146
  onClick={() => {
@@ -205,8 +199,7 @@ export function TagSearchInput<T = string>({
205
199
  "flex-1 min-w-[60px] bg-transparent border-none outline-none",
206
200
  "text-h4",
207
201
  "text-ods-text-primary placeholder:text-ods-text-secondary",
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",
202
+ "disabled:cursor-not-allowed",
210
203
  )}
211
204
  />
212
205
  </div>
@@ -119,12 +119,6 @@ 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",
128
122
  !hideBorder && isInvalid && cn("border-ods-error hover:border-ods-error", accentBorderError),
129
123
  hideBorder && "bg-transparent",
130
124
  )}
@@ -156,8 +150,7 @@ const Textarea = React.forwardRef<HTMLTextAreaElement, TextareaProps>(
156
150
  // internally instead of pushing the icon out of frame.
157
151
  "max-h-[160px] overflow-y-auto",
158
152
  "scrollbar-thin scrollbar-track-transparent scrollbar-thumb-ods-border/30 hover:scrollbar-thumb-ods-text-secondary/30",
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",
153
+ "disabled:cursor-not-allowed disabled:placeholder:text-ods-border",
161
154
  className,
162
155
  )}
163
156
  {...props}
@@ -1,7 +1,6 @@
1
1
  'use client'
2
2
 
3
- import { type ReactNode } from 'react'
4
- import { useIsTruncated } from '../../hooks/ui/use-is-truncated'
3
+ import React, { type ReactNode, useEffect, useRef, useState } from 'react'
5
4
  import { cn } from '../../utils/cn'
6
5
  import { FloatingTooltip } from './floating-tooltip'
7
6
 
@@ -70,8 +69,24 @@ export function TruncateText({
70
69
  tone = 'primary',
71
70
  mono = false,
72
71
  }: TruncateTextProps) {
72
+ const ref = useRef<HTMLSpanElement>(null)
73
+ const [isTruncated, setIsTruncated] = useState(false)
73
74
  const isMultiLine = lines > 1
74
- const { ref, isTruncated } = useIsTruncated<HTMLSpanElement>(children, { multiline: isMultiLine })
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])
75
90
 
76
91
  const clampClass = isMultiLine
77
92
  ? LINE_CLAMP_CLASS[lines as Exclude<typeof lines, 1>]
@@ -6,7 +6,6 @@ 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'
10
9
  export * from './use-local-storage'
11
10
  export * from './use-marquee-engine'
12
11
  export * from './use-media-query'