@flamingo-stack/openframe-frontend-core 0.0.490 → 0.0.491-snapshot.20260729083908
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-FY5XAW3H.cjs → chunk-2XAAFAQT.cjs} +18 -18
- package/dist/{chunk-FY5XAW3H.cjs.map → chunk-2XAAFAQT.cjs.map} +1 -1
- package/dist/{chunk-AIRNG27G.js → chunk-4AWRPTKE.js} +6 -6
- package/dist/{chunk-6CFFISUC.js → chunk-4J3CB4KO.js} +3 -3
- package/dist/{chunk-VUXNXWOQ.cjs → chunk-4MLOMMDX.cjs} +1900 -1600
- package/dist/chunk-4MLOMMDX.cjs.map +1 -0
- package/dist/{chunk-OQIMH2WQ.js → chunk-56J7RLMM.js} +1514 -1214
- package/dist/{chunk-OQIMH2WQ.js.map → chunk-56J7RLMM.js.map} +1 -1
- package/dist/{chunk-7I42V2OD.cjs → chunk-5ICPQAUI.cjs} +73 -73
- package/dist/{chunk-7I42V2OD.cjs.map → chunk-5ICPQAUI.cjs.map} +1 -1
- package/dist/{chunk-BZMOEWWS.js → chunk-5RHJVPL7.js} +6 -6
- package/dist/{chunk-RW2LPL72.js → chunk-6WTCJI5R.js} +2 -2
- package/dist/{chunk-VDZO6IDR.js → chunk-BTIRYGDT.js} +5 -5
- package/dist/{chunk-D5LQMRPU.cjs → chunk-DY6AJNVK.cjs} +5 -5
- package/dist/{chunk-D5LQMRPU.cjs.map → chunk-DY6AJNVK.cjs.map} +1 -1
- package/dist/{chunk-MPYDX5WX.js → chunk-FG5ALORQ.js} +4 -4
- package/dist/{chunk-2MKUHJ3O.cjs → chunk-J2J36PEN.cjs} +40 -40
- package/dist/{chunk-2MKUHJ3O.cjs.map → chunk-J2J36PEN.cjs.map} +1 -1
- package/dist/{chunk-AW63ET2A.cjs → chunk-LIRXKEOE.cjs} +19 -19
- package/dist/{chunk-AW63ET2A.cjs.map → chunk-LIRXKEOE.cjs.map} +1 -1
- package/dist/{chunk-UZZT65PQ.cjs → chunk-LVDI7R7T.cjs} +40 -40
- package/dist/{chunk-UZZT65PQ.cjs.map → chunk-LVDI7R7T.cjs.map} +1 -1
- package/dist/{chunk-GUTYUMGV.cjs → chunk-M4SCYPCR.cjs} +12 -12
- package/dist/{chunk-GUTYUMGV.cjs.map → chunk-M4SCYPCR.cjs.map} +1 -1
- package/dist/{chunk-GMPYR3MX.cjs → chunk-O6S6Q52W.cjs} +33 -33
- package/dist/{chunk-GMPYR3MX.cjs.map → chunk-O6S6Q52W.cjs.map} +1 -1
- package/dist/{chunk-IEZ3YBAP.cjs → chunk-OFFYASUY.cjs} +32 -32
- package/dist/{chunk-IEZ3YBAP.cjs.map → chunk-OFFYASUY.cjs.map} +1 -1
- package/dist/{chunk-NGEFYPY7.js → chunk-P3ITKTUE.js} +4 -4
- package/dist/{chunk-3IEVA5AO.cjs → chunk-PI5BMKV3.cjs} +93 -93
- package/dist/{chunk-3IEVA5AO.cjs.map → chunk-PI5BMKV3.cjs.map} +1 -1
- package/dist/{chunk-WLM3F3WU.js → chunk-QHRNQTW5.js} +3 -3
- package/dist/{chunk-GZK7FWJJ.js → chunk-QKWOBV55.js} +12 -12
- package/dist/chunk-QKWOBV55.js.map +1 -0
- package/dist/{chunk-STYA77PH.cjs → chunk-SGL6URIJ.cjs} +12 -12
- package/dist/chunk-SGL6URIJ.cjs.map +1 -0
- package/dist/{chunk-6QK3ONDD.js → chunk-SZWMJIH3.js} +2 -2
- 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-6CJ2K7BD.js → chunk-U5LU56JE.js} +3 -3
- package/dist/{chunk-EJQUYZXF.cjs → chunk-VML66FJQ.cjs} +12 -12
- package/dist/{chunk-EJQUYZXF.cjs.map → chunk-VML66FJQ.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-EGXCWZYP.js → chunk-WKWZ7LVC.js} +3 -3
- package/dist/{chunk-VL2YMDPH.js → chunk-WPFLV4QJ.js} +6 -6
- package/dist/components/case-studies/index.cjs +10 -10
- package/dist/components/case-studies/index.js +4 -4
- package/dist/components/chat/index.cjs +4 -4
- package/dist/components/chat/index.js +3 -3
- 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/floating-walkthrough-video.d.ts.map +1 -1
- 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 +161 -161
- package/dist/components/index.js +12 -12
- 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/navigation/navigation-sidebar-item.d.ts +23 -1
- package/dist/components/navigation/navigation-sidebar-item.d.ts.map +1 -1
- package/dist/components/navigation/navigation-sidebar.d.ts.map +1 -1
- 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/data-table/data-table-column-filter.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-header.d.ts.map +1 -1
- package/dist/components/ui/data-table/data-table-skeleton.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 +4 -4
- package/dist/components/ui/index.js +3 -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 +6 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +5 -3
- package/package.json +1 -1
- package/src/components/features/floating-walkthrough-video.tsx +3 -1
- 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/navigation/navigation-sidebar-item.tsx +80 -30
- package/src/components/navigation/navigation-sidebar.tsx +106 -46
- package/src/components/ui/.field-wrapper.md +5 -4
- package/src/components/ui/.tab-navigation.md +14 -1
- package/src/components/ui/__tests__/tab-navigation.test.tsx +159 -0
- 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/data-table/data-table-column-filter.tsx +7 -1
- package/src/components/ui/data-table/data-table-header.tsx +4 -1
- package/src/components/ui/data-table/data-table-skeleton.tsx +7 -0
- 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 +379 -158
- 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/dist/chunk-BIKOJ4FH.cjs.map +0 -1
- package/dist/chunk-GZK7FWJJ.js.map +0 -1
- package/dist/chunk-PB3BTZDU.js.map +0 -1
- package/dist/chunk-STYA77PH.cjs.map +0 -1
- package/dist/chunk-VUXNXWOQ.cjs.map +0 -1
- /package/dist/{chunk-AIRNG27G.js.map → chunk-4AWRPTKE.js.map} +0 -0
- /package/dist/{chunk-6CFFISUC.js.map → chunk-4J3CB4KO.js.map} +0 -0
- /package/dist/{chunk-BZMOEWWS.js.map → chunk-5RHJVPL7.js.map} +0 -0
- /package/dist/{chunk-RW2LPL72.js.map → chunk-6WTCJI5R.js.map} +0 -0
- /package/dist/{chunk-VDZO6IDR.js.map → chunk-BTIRYGDT.js.map} +0 -0
- /package/dist/{chunk-MPYDX5WX.js.map → chunk-FG5ALORQ.js.map} +0 -0
- /package/dist/{chunk-NGEFYPY7.js.map → chunk-P3ITKTUE.js.map} +0 -0
- /package/dist/{chunk-WLM3F3WU.js.map → chunk-QHRNQTW5.js.map} +0 -0
- /package/dist/{chunk-6QK3ONDD.js.map → chunk-SZWMJIH3.js.map} +0 -0
- /package/dist/{chunk-6CJ2K7BD.js.map → chunk-U5LU56JE.js.map} +0 -0
- /package/dist/{chunk-EGXCWZYP.js.map → chunk-WKWZ7LVC.js.map} +0 -0
- /package/dist/{chunk-VL2YMDPH.js.map → chunk-WPFLV4QJ.js.map} +0 -0
|
@@ -6,6 +6,7 @@ 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'
|
|
9
10
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from './tooltip'
|
|
10
11
|
|
|
11
12
|
export type PageActionButton = {
|
|
@@ -206,26 +207,104 @@ export interface PageActionsProps {
|
|
|
206
207
|
*/
|
|
207
208
|
selector?: React.ReactNode
|
|
208
209
|
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
|
|
209
216
|
}
|
|
210
217
|
|
|
211
218
|
const ACTIONS_GAP = 'gap-[var(--spacing-system-xs)]'
|
|
212
219
|
|
|
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
|
+
|
|
213
291
|
export function PageActions({
|
|
214
292
|
variant = 'icon-buttons',
|
|
215
293
|
actions,
|
|
216
294
|
menuActions,
|
|
217
295
|
selector,
|
|
218
296
|
className,
|
|
297
|
+
loading,
|
|
219
298
|
}: PageActionsProps) {
|
|
220
299
|
if (variant === 'icon-buttons') {
|
|
221
|
-
return <IconButtonsVariant actions={actions} menuActions={menuActions} selector={selector} className={className} />
|
|
300
|
+
return <IconButtonsVariant actions={actions} menuActions={menuActions} selector={selector} className={className} loading={loading} />
|
|
222
301
|
}
|
|
223
302
|
|
|
224
303
|
if (variant === 'menu-primary') {
|
|
225
|
-
return <MenuPrimaryVariant actions={actions} menuActions={menuActions || []} selector={selector} className={className} />
|
|
304
|
+
return <MenuPrimaryVariant actions={actions} menuActions={menuActions || []} selector={selector} className={className} loading={loading} />
|
|
226
305
|
}
|
|
227
306
|
|
|
228
|
-
return <PrimaryButtonsVariant actions={actions} className={className} />
|
|
307
|
+
return <PrimaryButtonsVariant actions={actions} className={className} loading={loading} />
|
|
229
308
|
}
|
|
230
309
|
|
|
231
310
|
function IconButtonsVariant({
|
|
@@ -233,11 +312,13 @@ function IconButtonsVariant({
|
|
|
233
312
|
menuActions,
|
|
234
313
|
selector,
|
|
235
314
|
className,
|
|
315
|
+
loading,
|
|
236
316
|
}: {
|
|
237
317
|
actions: PageActionButton[]
|
|
238
318
|
menuActions?: ActionsMenuGroup[]
|
|
239
319
|
selector?: React.ReactNode
|
|
240
320
|
className?: string
|
|
321
|
+
loading?: boolean
|
|
241
322
|
}) {
|
|
242
323
|
const desktopActions = actions.filter(a => !a.showOnlyMobile)
|
|
243
324
|
const hasMenuActions = !!menuActions && menuActions.some(g => g.items.length > 0)
|
|
@@ -257,17 +338,25 @@ function IconButtonsVariant({
|
|
|
257
338
|
{/* Desktop: every action as an icon button + optional overflow menu */}
|
|
258
339
|
<div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>
|
|
259
340
|
{selector}
|
|
260
|
-
{
|
|
261
|
-
<
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
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
|
+
)}
|
|
266
353
|
</div>
|
|
267
354
|
|
|
268
355
|
{/* Mobile: single icon button OR all actions merged into one "..." menu */}
|
|
269
356
|
<div className={cn('flex md:hidden', className)}>
|
|
270
|
-
{
|
|
357
|
+
{loading ? (
|
|
358
|
+
<MobileTriggerSkeleton />
|
|
359
|
+
) : useSingleActionMobile && singleAction ? (
|
|
271
360
|
renderActionButton(singleAction, { iconOnly: true })
|
|
272
361
|
) : hasMobileMenuItems ? (
|
|
273
362
|
<ActionsMenuDropdown groups={mobileMenuGroups} />
|
|
@@ -284,9 +373,11 @@ function IconButtonsVariant({
|
|
|
284
373
|
function PrimaryButtonsVariant({
|
|
285
374
|
actions,
|
|
286
375
|
className,
|
|
376
|
+
loading,
|
|
287
377
|
}: {
|
|
288
378
|
actions: PageActionButton[]
|
|
289
379
|
className?: string
|
|
380
|
+
loading?: boolean
|
|
290
381
|
}) {
|
|
291
382
|
// Sort: outline first, accent last (rightmost on desktop).
|
|
292
383
|
const sortedActions = [...actions].sort((a, b) => {
|
|
@@ -300,14 +391,20 @@ function PrimaryButtonsVariant({
|
|
|
300
391
|
return (
|
|
301
392
|
<>
|
|
302
393
|
<div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>
|
|
303
|
-
{
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
)
|
|
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
|
+
)}
|
|
308
405
|
</div>
|
|
309
406
|
|
|
310
|
-
<MobileBottomActions actions={sortedActions} />
|
|
407
|
+
<MobileBottomActions actions={sortedActions} loading={loading} />
|
|
311
408
|
</>
|
|
312
409
|
)
|
|
313
410
|
}
|
|
@@ -321,11 +418,13 @@ function MenuPrimaryVariant({
|
|
|
321
418
|
menuActions,
|
|
322
419
|
selector,
|
|
323
420
|
className,
|
|
421
|
+
loading,
|
|
324
422
|
}: {
|
|
325
423
|
actions: PageActionButton[]
|
|
326
424
|
menuActions: ActionsMenuGroup[]
|
|
327
425
|
selector?: React.ReactNode
|
|
328
426
|
className?: string
|
|
427
|
+
loading?: boolean
|
|
329
428
|
}) {
|
|
330
429
|
const desktopActions = actions.filter(a => !a.showOnlyMobile)
|
|
331
430
|
const hasMenuActions = menuActions.some(g => g.items.length > 0)
|
|
@@ -340,34 +439,47 @@ function MenuPrimaryVariant({
|
|
|
340
439
|
<>
|
|
341
440
|
<div className={cn('hidden md:flex items-center', ACTIONS_GAP, className)}>
|
|
342
441
|
{selector}
|
|
343
|
-
{
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
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
|
+
)}
|
|
349
454
|
</div>
|
|
350
455
|
|
|
351
456
|
<div className={cn('flex md:hidden', className)}>
|
|
352
|
-
{hasMobileMenuItems ? <ActionsMenuDropdown groups={mobileMenuGroups} /> : null}
|
|
457
|
+
{loading ? <MobileTriggerSkeleton /> : hasMobileMenuItems ? <ActionsMenuDropdown groups={mobileMenuGroups} /> : null}
|
|
353
458
|
</div>
|
|
354
459
|
</>
|
|
355
460
|
)
|
|
356
461
|
}
|
|
357
462
|
|
|
358
|
-
function MobileBottomActions({ actions }: { actions: PageActionButton[] }) {
|
|
463
|
+
function MobileBottomActions({ actions, loading }: { actions: PageActionButton[]; loading?: boolean }) {
|
|
359
464
|
return (
|
|
360
465
|
<div className={cn(
|
|
361
466
|
'fixed md:hidden bottom-0 left-0 right-0 z-50',
|
|
362
467
|
'bg-ods-card border-t border-ods-border',
|
|
363
|
-
|
|
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)]',
|
|
364
472
|
ACTIONS_GAP,
|
|
365
473
|
)}>
|
|
366
|
-
{
|
|
367
|
-
<
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
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
|
+
)}
|
|
371
483
|
</div>
|
|
372
484
|
)
|
|
373
485
|
}
|
|
@@ -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",
|