@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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- source-hash:
|
|
1
|
+
<!-- source-hash: 674794aef9f624e4f74a99dae56b9617 -->
|
|
2
2
|
A layout wrapper component that adds an optional label above and a status message below any form field, handling the visual chrome (label + feedback text) while letting the field itself remain unstyled.
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
@@ -11,15 +11,16 @@ A `forwardRef` component that wraps form fields with consistent label and feedba
|
|
|
11
11
|
| Prop | Type | Default | Description |
|
|
12
12
|
|------|------|---------|-------------|
|
|
13
13
|
| `label` | `string` | — | Label text rendered above the field |
|
|
14
|
-
| `error` | `string` | — | Status/feedback message rendered below the field |
|
|
14
|
+
| `error` | `string` | — | Status/feedback message rendered below the field (out of flow) |
|
|
15
15
|
| `errorVariant` | `"error" \| "warning" \| "success" \| "muted"` | `"error"` | Color variant for the feedback message |
|
|
16
16
|
| `className` | `string` | — | Additional class applied to the outer wrapper |
|
|
17
17
|
| `children` | `React.ReactNode` | — | The field element(s) to wrap |
|
|
18
18
|
|
|
19
19
|
**Behavior:**
|
|
20
20
|
- When neither `label` nor `error` is present, the wrapper renders as `contents` (no layout box introduced).
|
|
21
|
-
- When either exists, it renders as a full-width flex column.
|
|
22
|
-
- The feedback message
|
|
21
|
+
- When either exists, it renders as a full-width **relative** flex column.
|
|
22
|
+
- The feedback message is a single line, ellipsized on overflow; the full text is accessible via the native `title` tooltip.
|
|
23
|
+
- The message is positioned **absolutely** (`bottom-0` + `translate-y-full`), so showing or hiding it never changes the field's height — no layout shift on validation. The trade-off: it hangs BELOW the wrapper box and overlaps whatever follows, so a form stacking fields must leave at least ~20px of vertical gap (e.g. `gap-[var(--spacing-system-lf)]`) for it to land in clear space. `CheckboxBlock` uses the same treatment.
|
|
23
24
|
|
|
24
25
|
**Color Variants:**
|
|
25
26
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- source-hash:
|
|
1
|
+
<!-- source-hash: ef59a6aa7c2983608cd2d9c678c34323 -->
|
|
2
2
|
Accessible, scrollable tab navigation bar component with optional URL synchronization, overflow scroll shadows, and support for both controlled and URL-driven active tab state.
|
|
3
3
|
|
|
4
4
|
## Key Components
|
|
@@ -46,6 +46,19 @@ Configuration object for URL synchronization:
|
|
|
46
46
|
{(activeTab) => <ContentPanel tab={activeTab} />}
|
|
47
47
|
</TabNavigation>
|
|
48
48
|
|
|
49
|
+
// Render prop with the pending flag. The tab BODY is deferred
|
|
50
|
+
// (`useDeferredValue`), so the underline moves instantly while the body keeps
|
|
51
|
+
// showing the previous tab until the new one is ready — no Suspense-fallback
|
|
52
|
+
// flash. `isStale` is true during that window; mark the body so stale data
|
|
53
|
+
// doesn't read as fresh data.
|
|
54
|
+
<TabNavigation tabs={tabs} urlSync>
|
|
55
|
+
{(activeTab, { isStale }) => (
|
|
56
|
+
<div className={cn(isStale && 'opacity-60 transition-opacity')}>
|
|
57
|
+
<ContentPanel tab={activeTab} />
|
|
58
|
+
</div>
|
|
59
|
+
)}
|
|
60
|
+
</TabNavigation>
|
|
61
|
+
|
|
49
62
|
// Stretched tabs with forced gradients
|
|
50
63
|
<TabNavigation
|
|
51
64
|
tabs={tabs}
|
|
@@ -124,7 +124,8 @@ const innerInputStyles = cn(
|
|
|
124
124
|
"flex-1 min-w-[60px] bg-transparent border-none outline-none",
|
|
125
125
|
"text-h4",
|
|
126
126
|
"text-ods-text-primary placeholder:text-ods-text-secondary",
|
|
127
|
-
|
|
127
|
+
// Disabled - match Input exactly (value greys out, placeholder dims further)
|
|
128
|
+
"disabled:cursor-not-allowed disabled:text-ods-text-disabled disabled:placeholder:text-ods-border"
|
|
128
129
|
)
|
|
129
130
|
|
|
130
131
|
function AutocompleteInner<T = string>(
|
|
@@ -421,6 +422,9 @@ function AutocompleteInner<T = string>(
|
|
|
421
422
|
<PopoverPrimitive.Root open={isOpen} onOpenChange={handleOpenChange} modal={false}>
|
|
422
423
|
<PopoverPrimitive.Anchor asChild>
|
|
423
424
|
<div
|
|
425
|
+
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
426
|
+
// (and scrolls to) the first field that failed validation.
|
|
427
|
+
data-invalid={isInvalid || undefined}
|
|
424
428
|
className={cn(
|
|
425
429
|
// Layout — single line, no wrapping
|
|
426
430
|
"flex items-center rounded-[6px] border min-h-11 md:min-h-12 cursor-text",
|
|
@@ -430,6 +434,14 @@ function AutocompleteInner<T = string>(
|
|
|
430
434
|
"group",
|
|
431
435
|
!disabled && "hover:bg-ods-bg-hover hover:border-ods-border-hover active:bg-ods-bg-active active:border-ods-border-active",
|
|
432
436
|
disabled && "!cursor-not-allowed bg-ods-bg",
|
|
437
|
+
// Adornments / chevron carry their own colour — grey them with the
|
|
438
|
+
// value so a disabled field reads as one flat colour. Scoped to the
|
|
439
|
+
// DIRECT span child (the start adornment) on purpose: as a descendant
|
|
440
|
+
// rule it also hit the selected `Tag` chips in the middle zone, whose
|
|
441
|
+
// label is a span, and they went invisible against their own fill in
|
|
442
|
+
// the light theme. Anything nested inside the adornment that sets its
|
|
443
|
+
// own colour keeps it; anything that doesn't inherits the grey.
|
|
444
|
+
"has-[:disabled]:[&>span]:text-ods-text-disabled has-[:disabled]:[&_svg]:text-ods-text-disabled",
|
|
433
445
|
isOpen && !isInvalid && "border-ods-accent hover:border-ods-accent",
|
|
434
446
|
isInvalid && "border-ods-error hover:border-ods-error"
|
|
435
447
|
)}
|
|
@@ -30,7 +30,7 @@ const buttonVariants = cva(
|
|
|
30
30
|
overlay: buttonSurfaceClasses.overlay,
|
|
31
31
|
},
|
|
32
32
|
size: {
|
|
33
|
-
default: "py-[var(--spacing-system-sf)] px-[var(--spacing-system-m)] text-h3 md:h-12 h-
|
|
33
|
+
default: "py-[var(--spacing-system-sf)] px-[var(--spacing-system-m)] text-h3 md:h-12 h-11",
|
|
34
34
|
small: "p-[var(--spacing-system-xs)] text-h5 h-6 md:h-8",
|
|
35
35
|
"small-legacy": "py-[var(--spacing-system-xs)] px-[var(--spacing-system-m)] h-10 text-[14px] font-bold", // Temporary alias for "small" — deprecated; grep size="small-legacy" (lib + hub) and migrate the remaining consumers before removal
|
|
36
36
|
// 24px pill for slim strips (announcement/promo bars, inline banner
|
|
@@ -36,6 +36,9 @@ const CheckboxBlock = React.forwardRef<
|
|
|
36
36
|
<div className={cn("relative flex w-full flex-col", className)}>
|
|
37
37
|
<label
|
|
38
38
|
htmlFor={id}
|
|
39
|
+
// Same marker every field exposes — it is how a form finds (and scrolls
|
|
40
|
+
// to) the first control that failed validation.
|
|
41
|
+
data-invalid={error ? true : undefined}
|
|
39
42
|
className={cn(
|
|
40
43
|
"flex items-center gap-[var(--spacing-system-s)] rounded-md ring-1 ring-inset w-full",
|
|
41
44
|
// Trailing content stacks full-width below the text on mobile.
|
|
@@ -45,8 +48,17 @@ const CheckboxBlock = React.forwardRef<
|
|
|
45
48
|
description && "min-h-[60px] md:min-h-[64px]",
|
|
46
49
|
"bg-ods-card ring-ods-border",
|
|
47
50
|
"cursor-pointer transition-colors duration-200",
|
|
48
|
-
|
|
49
|
-
|
|
51
|
+
// States mirror Input: hover / active move the BACKGROUND, the ring
|
|
52
|
+
// stays put. Disabled swaps to the flat `ods-bg` fill and greys the
|
|
53
|
+
// text + the box instead of fading the whole block, so a disabled block
|
|
54
|
+
// reads the same as a disabled field. The greying itself lives on the
|
|
55
|
+
// TEXT COLUMN below, not here: as a descendant rule on the label it also
|
|
56
|
+
// repainted the checked INDICATOR (Radix renders it as a `span`, and the
|
|
57
|
+
// checkmark svg inherits `currentColor`) and whatever `trailing` renders
|
|
58
|
+
// — a `Tag`'s label is a span too, and it went invisible against its own
|
|
59
|
+
// fill in the light theme.
|
|
60
|
+
!disabled && "hover:bg-ods-bg-hover active:bg-ods-bg-active",
|
|
61
|
+
disabled && "cursor-not-allowed bg-ods-bg",
|
|
50
62
|
error && "ring-ods-error",
|
|
51
63
|
)}
|
|
52
64
|
>
|
|
@@ -63,7 +75,11 @@ const CheckboxBlock = React.forwardRef<
|
|
|
63
75
|
error ? "border-ods-error" : "border-[var(--color-border-strong)]",
|
|
64
76
|
"bg-ods-card",
|
|
65
77
|
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ods-accent focus-visible:ring-offset-2 focus-visible:ring-offset-ods-card",
|
|
78
|
+
// Disabled: the EMPTY box dims with the text instead of the whole
|
|
79
|
+
// block fading. A checked box keeps its accent fill — greying it too
|
|
80
|
+
// would bury the checkmark, and "on but locked" has to stay readable.
|
|
66
81
|
"disabled:cursor-not-allowed",
|
|
82
|
+
"disabled:data-[state=unchecked]:bg-ods-bg disabled:data-[state=unchecked]:border-ods-border",
|
|
67
83
|
"data-[state=checked]:bg-[var(--color-accent-primary)] data-[state=checked]:border-[var(--color-accent-primary)]"
|
|
68
84
|
)}
|
|
69
85
|
>
|
|
@@ -73,7 +89,15 @@ const CheckboxBlock = React.forwardRef<
|
|
|
73
89
|
<CheckboxCheckmarkIcon className="w-2 h-2 md:w-2.5 md:h-2.5" />
|
|
74
90
|
</CheckboxPrimitive.Indicator>
|
|
75
91
|
</CheckboxPrimitive.Root>
|
|
76
|
-
<div
|
|
92
|
+
<div
|
|
93
|
+
className={cn(
|
|
94
|
+
"flex flex-1 flex-col justify-center min-w-0",
|
|
95
|
+
// Direct children only — these are the two component-owned wrappers.
|
|
96
|
+
// `label` and `description` are ReactNode (rich text, inline links),
|
|
97
|
+
// so a descendant rule would repaint content the caller styled itself.
|
|
98
|
+
disabled && "[&>span]:text-ods-text-disabled",
|
|
99
|
+
)}
|
|
100
|
+
>
|
|
77
101
|
<span className={cn(
|
|
78
102
|
"text-h4 !leading-5 md:!leading-6",
|
|
79
103
|
"text-ods-text-primary select-none",
|
|
@@ -451,8 +451,12 @@ const triggerButtonStyles = cn(
|
|
|
451
451
|
// Hover & active (not disabled)
|
|
452
452
|
"enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-active",
|
|
453
453
|
"focus:outline-none",
|
|
454
|
-
// Disabled
|
|
454
|
+
// Disabled - match Input exactly. The value/placeholder span inside sets its
|
|
455
|
+
// own colour, so the child rule (higher specificity) greys it too. Scoped to
|
|
456
|
+
// DIRECT children like every other field, so nested content that owns its
|
|
457
|
+
// colour keeps it.
|
|
455
458
|
"disabled:!cursor-not-allowed disabled:bg-ods-bg",
|
|
459
|
+
"disabled:text-ods-text-disabled disabled:[&>span]:text-ods-text-disabled disabled:[&_svg]:text-ods-text-disabled",
|
|
456
460
|
// Animation
|
|
457
461
|
"transition-colors duration-200"
|
|
458
462
|
);
|
|
@@ -465,6 +469,7 @@ const timeSelectTriggerStyles = cn(
|
|
|
465
469
|
"enabled:hover:bg-ods-bg-hover enabled:hover:border-ods-border-hover enabled:active:bg-ods-bg-active enabled:active:border-ods-border-active",
|
|
466
470
|
"focus:outline-none",
|
|
467
471
|
"disabled:!cursor-not-allowed disabled:bg-ods-bg",
|
|
472
|
+
"disabled:text-ods-text-disabled disabled:[&>span]:text-ods-text-disabled disabled:[&_svg]:text-ods-text-disabled",
|
|
468
473
|
"transition-colors duration-200 cursor-pointer",
|
|
469
474
|
"text-ods-text-primary"
|
|
470
475
|
);
|
|
@@ -519,6 +524,9 @@ export function DatePicker(props: DatePickerProps) {
|
|
|
519
524
|
<button
|
|
520
525
|
type="button"
|
|
521
526
|
disabled={disabled}
|
|
527
|
+
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
528
|
+
// (and scrolls to) the first field that failed validation.
|
|
529
|
+
data-invalid={isInvalid || undefined}
|
|
522
530
|
className={cn(triggerButtonStyles, "group", open && !isInvalid && "border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card", isInvalid && "border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card", className)}
|
|
523
531
|
>
|
|
524
532
|
<Calendar className="size-6 text-ods-text-secondary shrink-0" />
|
|
@@ -692,6 +700,9 @@ export function DatePickerInput({
|
|
|
692
700
|
<button
|
|
693
701
|
type="button"
|
|
694
702
|
disabled={disabled}
|
|
703
|
+
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
704
|
+
// (and scrolls to) the first field that failed validation.
|
|
705
|
+
data-invalid={isInvalid || undefined}
|
|
695
706
|
className={cn(triggerButtonStyles, "group", open && !isInvalid && "border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card", isInvalid && "border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card", "flex-1")}
|
|
696
707
|
>
|
|
697
708
|
<Calendar className="size-6 text-ods-text-secondary shrink-0" />
|
|
@@ -923,6 +934,9 @@ export function DatePickerInputSimple({
|
|
|
923
934
|
<button
|
|
924
935
|
type="button"
|
|
925
936
|
disabled={disabled}
|
|
937
|
+
// Same marker Input/Textarea/Select expose — it is how a form finds
|
|
938
|
+
// (and scrolls to) the first field that failed validation.
|
|
939
|
+
data-invalid={isInvalid || undefined}
|
|
926
940
|
className={cn(triggerButtonStyles, "group", open && !isInvalid && "border-ods-accent enabled:hover:border-ods-accent enabled:hover:bg-ods-card", isInvalid && "border-ods-error enabled:hover:border-ods-error enabled:hover:bg-ods-card", "flex-1")}
|
|
927
941
|
>
|
|
928
942
|
<Calendar className="size-6 text-ods-text-secondary shrink-0" />
|
|
@@ -6,7 +6,16 @@ import { cn } from "../../utils/cn"
|
|
|
6
6
|
export interface FieldWrapperProps {
|
|
7
7
|
/** Label text displayed above the field */
|
|
8
8
|
label?: string
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Status message displayed below the field, on ONE line, ellipsized on
|
|
11
|
+
* overflow (full text via the native `title`).
|
|
12
|
+
*
|
|
13
|
+
* It is positioned OUT OF FLOW (absolute, hanging below the wrapper), so it
|
|
14
|
+
* never changes the field's height when it appears — the same treatment
|
|
15
|
+
* `CheckboxBlock` uses. The trade is that it overlaps whatever sits directly
|
|
16
|
+
* below: a form stacking fields needs at least ~20px of vertical gap (e.g.
|
|
17
|
+
* `gap-[var(--spacing-system-lf)]`) for the message to land in clear space.
|
|
18
|
+
*/
|
|
10
19
|
error?: string
|
|
11
20
|
/** Color variant for the message: "error" (red), "warning" (yellow), "success" (green) or "muted" (grey) */
|
|
12
21
|
errorVariant?: "error" | "warning" | "success" | "muted"
|
|
@@ -27,7 +36,7 @@ const FieldWrapper = React.forwardRef<HTMLDivElement, FieldWrapperProps>(
|
|
|
27
36
|
const hasChrome = label != null || error != null
|
|
28
37
|
|
|
29
38
|
return (
|
|
30
|
-
<div ref={ref} className={cn(hasChrome ? "flex w-full flex-col" : "contents", className)}>
|
|
39
|
+
<div ref={ref} className={cn(hasChrome ? "relative flex w-full flex-col" : "contents", className)}>
|
|
31
40
|
{label && (
|
|
32
41
|
<label className="text-h4 text-ods-text-primary mb-1">
|
|
33
42
|
{label}
|
|
@@ -35,10 +44,7 @@ const FieldWrapper = React.forwardRef<HTMLDivElement, FieldWrapperProps>(
|
|
|
35
44
|
)}
|
|
36
45
|
{children}
|
|
37
46
|
{error && (
|
|
38
|
-
<p
|
|
39
|
-
className={cn("mt-1 text-h6 break-words line-clamp-2", errorVariantClasses[errorVariant])}
|
|
40
|
-
title={error}
|
|
41
|
-
>
|
|
47
|
+
<p className={cn("absolute bottom-0 left-0 right-0 translate-y-full text-h6 truncate", errorVariantClasses[errorVariant])} title={error}>
|
|
42
48
|
{error}
|
|
43
49
|
</p>
|
|
44
50
|
)}
|
|
@@ -26,6 +26,13 @@ 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
|
|
29
36
|
}
|
|
30
37
|
|
|
31
38
|
// Parse colored text markup like [YELLOW]text[/YELLOW] into JSX
|
|
@@ -85,6 +92,7 @@ export function FloatingTooltip({
|
|
|
85
92
|
className,
|
|
86
93
|
delayDuration = 0,
|
|
87
94
|
disabled = false,
|
|
95
|
+
triggerClassName,
|
|
88
96
|
}: FloatingTooltipProps) {
|
|
89
97
|
const [isOpen, setIsOpen] = React.useState(false)
|
|
90
98
|
const arrowRef = React.useRef<HTMLDivElement>(null)
|
|
@@ -151,7 +159,7 @@ export function FloatingTooltip({
|
|
|
151
159
|
|
|
152
160
|
return (
|
|
153
161
|
<>
|
|
154
|
-
<div ref={refs.setReference} {...getReferenceProps()}>
|
|
162
|
+
<div ref={refs.setReference} className={triggerClassName} {...getReferenceProps()}>
|
|
155
163
|
{children}
|
|
156
164
|
</div>
|
|
157
165
|
<FloatingPortal>
|
|
@@ -52,7 +52,14 @@ 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
|
-
|
|
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",
|
|
56
63
|
"transition-colors duration-200",
|
|
57
64
|
className,
|
|
58
65
|
)}
|
|
@@ -83,8 +83,15 @@ 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
|
|
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).
|
|
87
93
|
props.disabled && "!cursor-not-allowed bg-ods-bg",
|
|
94
|
+
"has-[:disabled]:[&>span]:text-ods-text-disabled has-[:disabled]:[&_svg]:text-ods-text-disabled",
|
|
88
95
|
// Invalid
|
|
89
96
|
isInvalid && invalidBorderClasses[variantIsInvalid ? (errorVariant as "error" | "warning") : "error"],
|
|
90
97
|
className
|
|
@@ -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
|
}
|