@flamingo-stack/openframe-frontend-core 0.0.732 → 0.0.734
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-YGKVHSNQ.cjs → chunk-2OHBH6PS.cjs} +42 -42
- package/dist/{chunk-MEDDKM3A.cjs → chunk-3HPB4H4C.cjs} +2 -2
- package/dist/{chunk-BS4EZZCN.cjs → chunk-3NANWZO3.cjs} +15 -15
- package/dist/{chunk-5HMGK3N6.cjs → chunk-4Q34V7MD.cjs} +20 -20
- package/dist/{chunk-22ZK7BA3.cjs → chunk-4V672TEI.cjs} +1127 -1072
- package/dist/{chunk-OI7VCXYU.cjs → chunk-5DDFSGCE.cjs} +40 -40
- package/dist/{chunk-CUF3IYIB.cjs → chunk-5OGT7H2S.cjs} +28 -28
- package/dist/{chunk-C5NERNE6.cjs → chunk-73ZW3265.cjs} +93 -93
- package/dist/{chunk-GID62YEN.cjs → chunk-AAFW4STC.cjs} +3 -3
- package/dist/{chunk-MVZ6XFPK.cjs → chunk-AOTOOGNX.cjs} +8 -8
- package/dist/{chunk-SJRR745G.cjs → chunk-BP6R5WDW.cjs} +44 -44
- package/dist/{chunk-OJ3XU3YZ.cjs → chunk-CMUEFHM3.cjs} +4 -4
- package/dist/{chunk-3BLBWZ6A.cjs → chunk-D34BLEIR.cjs} +7 -7
- package/dist/{chunk-XOQ2UUVS.cjs → chunk-DHIISS4W.cjs} +50 -40
- package/dist/{chunk-RIXGLS5Y.cjs → chunk-DUVOQHQI.cjs} +4 -4
- package/dist/{chunk-E3N24573.cjs → chunk-DUXLZPMJ.cjs} +9 -9
- package/dist/{chunk-6KDGIOLP.cjs → chunk-EMBH2EJO.cjs} +89 -89
- package/dist/{chunk-N466AJGN.cjs → chunk-FA5SMA7L.cjs} +17 -17
- package/dist/{chunk-AEVGCB3Y.cjs → chunk-FCGLSWKR.cjs} +4 -4
- package/dist/{chunk-MANNIJ42.cjs → chunk-FICJS5OA.cjs} +6 -0
- package/dist/{chunk-KLXHCNDC.cjs → chunk-FTTLBMHU.cjs} +3 -3
- package/dist/{chunk-7DUXR4HL.cjs → chunk-GVTDVS5U.cjs} +3 -3
- package/dist/{chunk-ZWMPAFGB.cjs → chunk-HAWQA5FI.cjs} +236 -236
- package/dist/{chunk-BDTX2OEE.cjs → chunk-JRC3KXKV.cjs} +32 -32
- package/dist/{chunk-PRWBVIWW.cjs → chunk-KSXZABGH.cjs} +59 -59
- package/dist/{chunk-G37LT2ID.cjs → chunk-L4ZNR5V7.cjs} +38 -38
- package/dist/{chunk-I46MQ4R5.cjs → chunk-L7WJBJU7.cjs} +73 -73
- package/dist/{chunk-NYY25425.cjs → chunk-MYBTNOS6.cjs} +2 -2
- package/dist/{chunk-HELVKMRG.cjs → chunk-NTJE6SM6.cjs} +2 -2
- package/dist/{chunk-5HSMTSHU.cjs → chunk-OV7QQHLG.cjs} +24 -24
- package/dist/{chunk-6HHUU56Y.cjs → chunk-R65UD6BH.cjs} +14 -14
- package/dist/{chunk-CC3LINHF.cjs → chunk-SA7LDEVJ.cjs} +3 -3
- package/dist/{chunk-TLDCSTTQ.cjs → chunk-TLQQP2EL.cjs} +10 -10
- package/dist/{chunk-QMTC3BK3.cjs → chunk-ZJNYWNCB.cjs} +21 -21
- package/dist/{chunk-HH4BMYDX.cjs → chunk-ZRYKDN2J.cjs} +19 -19
- package/dist/{cloud-tenants-ILRE74JY.cjs → cloud-tenants-HU7YZD33.cjs} +21 -21
- package/dist/components/case-studies/index.cjs +34 -34
- package/dist/components/chat/embeddable-chat.d.ts +16 -0
- package/dist/components/chat/embeddable-chat.d.ts.map +1 -1
- package/dist/components/chat/embeddable-chat.js +5 -2
- package/dist/components/chat/index.cjs +15 -15
- package/dist/components/chat/utils/icon-library.d.ts.map +1 -1
- package/dist/components/chat/utils/icon-library.js +6 -0
- package/dist/components/contact/index.cjs +22 -22
- package/dist/components/docs/index.cjs +25 -25
- package/dist/components/embeds/index.cjs +22 -22
- package/dist/components/faq/index.cjs +22 -22
- package/dist/components/features/index.cjs +21 -21
- package/dist/components/help-center-pages/index.cjs +137 -137
- package/dist/components/index.cjs +76 -76
- package/dist/components/layout/page-heading.d.ts +9 -1
- package/dist/components/layout/page-heading.d.ts.map +1 -1
- package/dist/components/layout/page-heading.js +15 -5
- package/dist/components/meeting-scheduler/index.cjs +63 -63
- package/dist/components/navigation/index.cjs +24 -24
- package/dist/components/onboarding-guides/index.cjs +24 -24
- package/dist/components/product-demo/index.cjs +8 -8
- package/dist/components/related-content/index.cjs +9 -9
- package/dist/components/tickets/index.cjs +25 -25
- package/dist/components/ui/index.cjs +21 -21
- package/dist/components/ui/searchable-select.d.ts +29 -3
- package/dist/components/ui/searchable-select.d.ts.map +1 -1
- package/dist/components/ui/searchable-select.js +101 -45
- package/dist/{devices-A6SFNGE4.cjs → devices-CKLN7BHY.cjs} +22 -22
- package/dist/{embeddable-chat-BKPYNOIM.cjs → embeddable-chat-FTAX4SVV.cjs} +95 -92
- package/dist/hooks/index.cjs +8 -8
- package/dist/index.cjs +27 -27
- package/dist/{knowledge-5ZCVKUYV.cjs → knowledge-NRLHSCC5.cjs} +22 -22
- package/dist/{logs-CL4OJASB.cjs → logs-BR6SLYAF.cjs} +29 -29
- package/dist/{policies-7D44ZVS5.cjs → policies-MQRXBYG2.cjs} +25 -25
- package/dist/{remote-session-QB3CIZRM.cjs → remote-session-Z4YZFG5E.cjs} +9 -9
- package/dist/{software-update-AX6TZL26.cjs → software-update-RUGIQP7N.cjs} +25 -25
- package/dist/{tickets-board-7FAV2YWF.cjs → tickets-board-JOSYLOS7.cjs} +12 -12
- package/package.json +1 -1
- package/src/components/chat/embeddable-chat.tsx +29 -2
- package/src/components/chat/utils/icon-library.tsx +7 -0
- package/src/components/layout/page-heading.tsx +37 -7
- package/src/components/ui/searchable-select.tsx +124 -32
|
@@ -481,6 +481,22 @@ export interface EmbeddableChatProps {
|
|
|
481
481
|
items: ChatContextItem[];
|
|
482
482
|
onRemove?: (item: ChatContextItem) => void;
|
|
483
483
|
};
|
|
484
|
+
/**
|
|
485
|
+
* Host content pinned between the thread and the composer: the thread
|
|
486
|
+
* scrolls above it, the composer stays below, and it keeps its place while
|
|
487
|
+
* the thread grows (e.g. a guided tour the host attaches to one
|
|
488
|
+
* conversation, deciding by `mingoState.activeDialogId`). Rendered wherever
|
|
489
|
+
* the composer is - a conversation or the new-chat surface - and never on
|
|
490
|
+
* the chat list. Sized by its content: a tall block is the host's to cap or
|
|
491
|
+
* scroll. `null`/unset leaves the layout as it was.
|
|
492
|
+
*/
|
|
493
|
+
aboveComposer?: React.ReactNode;
|
|
494
|
+
/**
|
|
495
|
+
* v2: what the list's "What to ask Mingo?" opens. Default: a new chat, the
|
|
496
|
+
* same as "Start New Chat"; a host with its own suggestions surface (a
|
|
497
|
+
* guided tour in a conversation of its own) points it there instead.
|
|
498
|
+
*/
|
|
499
|
+
onWhatToAsk?: () => void;
|
|
484
500
|
}
|
|
485
501
|
|
|
486
502
|
// =============================================================================
|
|
@@ -1045,6 +1061,8 @@ function EmbeddableChatInner({
|
|
|
1045
1061
|
guidePendingPrompt,
|
|
1046
1062
|
onGuidePromptConsumed,
|
|
1047
1063
|
contextMemory,
|
|
1064
|
+
aboveComposer,
|
|
1065
|
+
onWhatToAsk,
|
|
1048
1066
|
handleRef,
|
|
1049
1067
|
}: EmbeddableChatProps & { handleRef?: React.Ref<EmbeddableChatHandle> }) {
|
|
1050
1068
|
// `shell` 'none' / 'inline' means the consumer hosts us inside their own panel
|
|
@@ -2397,8 +2415,8 @@ function EmbeddableChatInner({
|
|
|
2397
2415
|
onNewChat: leaveArchiveThen(handleNewChat),
|
|
2398
2416
|
onOpenArchive: headerOnOpenArchive && (archiveOpen ? closeArchive : headerOnOpenArchive),
|
|
2399
2417
|
archiveActive: archiveOpen,
|
|
2400
|
-
// The suggestions a new chat opens on.
|
|
2401
|
-
onWhatToAsk: leaveArchiveThen(handleNewChat),
|
|
2418
|
+
// The suggestions a new chat opens on, unless the host has a surface of its own.
|
|
2419
|
+
onWhatToAsk: leaveArchiveThen(onWhatToAsk ?? handleNewChat),
|
|
2402
2420
|
statusOf: dialogStatusOf,
|
|
2403
2421
|
onRequestRename: mingoCaps.canRename ? setRenameTarget : undefined,
|
|
2404
2422
|
onRequestArchive: mingoCaps.canArchive ? setArchiveTarget : undefined,
|
|
@@ -2987,6 +3005,15 @@ function EmbeddableChatInner({
|
|
|
2987
3005
|
</div>
|
|
2988
3006
|
</div>
|
|
2989
3007
|
|
|
3008
|
+
{/* Host block pinned above the composer (`aboveComposer`):
|
|
3009
|
+
the thread's own bottom padding separates them, the
|
|
3010
|
+
composer's horizontal inset is repeated so the two line up. */}
|
|
3011
|
+
{aboveComposer != null && aboveComposer !== false && (
|
|
3012
|
+
<div className="shrink-0 px-[var(--spacing-system-m)] pb-[var(--spacing-system-m)]">
|
|
3013
|
+
{aboveComposer}
|
|
3014
|
+
</div>
|
|
3015
|
+
)}
|
|
3016
|
+
|
|
2990
3017
|
<ChatAttachmentChipStrip
|
|
2991
3018
|
attachments={stagedAttachments}
|
|
2992
3019
|
onRemove={removeAttachment}
|
|
@@ -90,6 +90,13 @@ export const ICON_ALIASES: Readonly<Record<string, IconRef>> = {
|
|
|
90
90
|
video: { set: 'VideoRecorderIcon' },
|
|
91
91
|
headphones: { set: 'HeadphoneIcon' },
|
|
92
92
|
|
|
93
|
+
// The two LLM brands offered by `ICON_OPTIONS`. Their brand-colored glyphs
|
|
94
|
+
// are the only picker entries that hardcode a fill (#000 / #D97757), so they
|
|
95
|
+
// ignored the chip accent and sat black and orange in a cyan grid; the grey
|
|
96
|
+
// variants fill with `currentColor` like every other brand logo here.
|
|
97
|
+
'openai-logo': { set: 'OpenaiLogoGreyIcon' },
|
|
98
|
+
'anthropic-logo': { set: 'AnthropicLogoGreyIcon' },
|
|
99
|
+
|
|
93
100
|
// Figma-canonical aliases (node 7363:205938) — kept for Storybook
|
|
94
101
|
// fixtures and any older feed entries still using the long names.
|
|
95
102
|
'clickup-logo': CLICKUP_GREY,
|
|
@@ -154,7 +154,15 @@ const SECTION_HEADING_LAYOUT = {
|
|
|
154
154
|
} as const;
|
|
155
155
|
|
|
156
156
|
export interface SectionHeadingProps {
|
|
157
|
-
/**
|
|
157
|
+
/**
|
|
158
|
+
* The small label above the heading (Azeret Mono, uppercase, accent colour).
|
|
159
|
+
* On a page heading (`level="page"`) it is part of the `<h1>`: a hero's
|
|
160
|
+
* eyebrow names what the page is about ("Endpoint management software") above
|
|
161
|
+
* its positioning line, so the page's heading, and its accessible name, say
|
|
162
|
+
* both. The element takes no box of its own, so the eyebrow and the title keep
|
|
163
|
+
* their two places in the stack. On a section heading it stays a label beside
|
|
164
|
+
* the `<h2>`, whose accessible name is its title alone.
|
|
165
|
+
*/
|
|
158
166
|
eyebrow?: ReactNode;
|
|
159
167
|
/** The heading text; a node when part of it is highlighted. Every sentence mark in it is drawn in the accent colour. */
|
|
160
168
|
title: ReactNode;
|
|
@@ -204,14 +212,36 @@ export function SectionHeading({
|
|
|
204
212
|
const accentClassName = headingAccentClass(platform, tone);
|
|
205
213
|
const layout = SECTION_HEADING_LAYOUT[level];
|
|
206
214
|
const Tag = presentational ? 'div' : level === 'page' ? 'h1' : 'h2';
|
|
215
|
+
const eyebrowNode = eyebrow ? <span className={`text-h5 ${accentClassName}`}>{eyebrow}</span> : null;
|
|
216
|
+
const headingText = (
|
|
217
|
+
<>
|
|
218
|
+
{accentSentenceMarks(title, platform, tone)}
|
|
219
|
+
{punctuation ? <span className={accentClassName}>{punctuation}</span> : null}
|
|
220
|
+
</>
|
|
221
|
+
);
|
|
207
222
|
const stack = (
|
|
208
223
|
<div className={layout.stack}>
|
|
209
|
-
{
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
224
|
+
{eyebrowNode && level === 'page' ? (
|
|
225
|
+
// `contents`: the element names the heading and draws no box, so the stack lays its two children out as before.
|
|
226
|
+
<Tag className="contents">
|
|
227
|
+
{eyebrowNode}
|
|
228
|
+
<span className={`block ${layout.heading}`} data-section-heading>
|
|
229
|
+
{headingText}
|
|
230
|
+
</span>
|
|
231
|
+
</Tag>
|
|
232
|
+
) : (
|
|
233
|
+
<>
|
|
234
|
+
{eyebrowNode}
|
|
235
|
+
<Tag className={layout.heading} data-section-heading>
|
|
236
|
+
{headingText}
|
|
237
|
+
</Tag>
|
|
238
|
+
</>
|
|
239
|
+
)}
|
|
240
|
+
{intro ? (
|
|
241
|
+
<div className={layout.intro} data-section-intro>
|
|
242
|
+
{intro}
|
|
243
|
+
</div>
|
|
244
|
+
) : null}
|
|
215
245
|
</div>
|
|
216
246
|
);
|
|
217
247
|
if (!action) return className ? <div className={className}>{stack}</div> : stack;
|
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import * as PopoverPrimitive from '@radix-ui/react-popover';
|
|
4
|
-
import { type ReactNode, useMemo, useState } from 'react';
|
|
4
|
+
import { type ReactNode, useId, useMemo, useState } from 'react';
|
|
5
5
|
import { useKeyboardCollisionPadding } from '../../hooks/ui/use-keyboard-collision-padding';
|
|
6
6
|
import { cn } from '../../utils/cn';
|
|
7
7
|
import { CheckIcon, SearchIcon } from '../icons-v2-generated';
|
|
8
8
|
import { Chevron02DownIcon } from '../icons-v2-generated/arrows/chevron-02-down-icon';
|
|
9
|
+
import { FieldWrapper } from './field-wrapper';
|
|
9
10
|
import { Input } from './input';
|
|
10
11
|
|
|
11
12
|
export interface SearchableSelectOption {
|
|
12
13
|
value: string;
|
|
13
14
|
label: string;
|
|
14
|
-
/** Optional leading visual for the option row (avatar, icon, badge).
|
|
15
|
+
/** Optional leading visual for the option row (avatar, icon, badge). The
|
|
16
|
+
* default trigger leads with the selected option's icon too. */
|
|
15
17
|
icon?: ReactNode;
|
|
18
|
+
/** Extra terms the search matches besides `label` — e.g. the stored key of a
|
|
19
|
+
* glyph (`chart-pie`) whose label reads "Pie chart". */
|
|
20
|
+
keywords?: string[];
|
|
16
21
|
}
|
|
17
22
|
|
|
23
|
+
/** How the dropdown lays its options out — see `SearchableSelectProps.layout`. */
|
|
24
|
+
export type SearchableSelectLayout = 'list' | 'grid';
|
|
25
|
+
|
|
18
26
|
export interface SearchableSelectProps {
|
|
19
27
|
options: SearchableSelectOption[];
|
|
20
28
|
/** Currently selected option value; null/undefined when nothing is selected. */
|
|
@@ -34,7 +42,27 @@ export interface SearchableSelectProps {
|
|
|
34
42
|
* is rendered.
|
|
35
43
|
*/
|
|
36
44
|
trigger?: ReactNode;
|
|
37
|
-
/**
|
|
45
|
+
/** `id` of the default trigger button, which `label` names through `htmlFor`; generated when omitted. */
|
|
46
|
+
id?: string;
|
|
47
|
+
/**
|
|
48
|
+
* Label above the default trigger — the same `FieldWrapper` treatment
|
|
49
|
+
* `SelectTrigger` gives its `label`, so a searchable field lines up with the
|
|
50
|
+
* plain selects beside it. A custom `trigger` owns its own label.
|
|
51
|
+
*/
|
|
52
|
+
label?: string;
|
|
53
|
+
/** Label scale forwarded to FieldWrapper ('large' = text-h4 for designs with body-scale field titles). */
|
|
54
|
+
labelVariant?: 'default' | 'large';
|
|
55
|
+
/** Validation message under the default trigger; also paints its border red. */
|
|
56
|
+
error?: string;
|
|
57
|
+
/**
|
|
58
|
+
* `'list'` (default) stacks full-width rows. `'grid'` tiles the options —
|
|
59
|
+
* icon above a centered caption, `minmax(7rem, 1fr)` columns — the
|
|
60
|
+
* icon-picker pattern. The grid fills the popover, so size it through
|
|
61
|
+
* `contentClassName` (360px fits three tiles).
|
|
62
|
+
*/
|
|
63
|
+
layout?: SearchableSelectLayout;
|
|
64
|
+
/** Custom option-row renderer; default is icon + label + selected check
|
|
65
|
+
* (icon above the caption in the grid layout). */
|
|
38
66
|
renderOption?: (option: SearchableSelectOption, isSelected: boolean) => ReactNode;
|
|
39
67
|
align?: 'start' | 'center' | 'end';
|
|
40
68
|
/** Classes for the default trigger button. */
|
|
@@ -50,22 +78,37 @@ export interface SearchableSelectProps {
|
|
|
50
78
|
* custom `trigger` for icon-button use cases; the default trigger looks and
|
|
51
79
|
* behaves like `SelectTrigger`.
|
|
52
80
|
*/
|
|
53
|
-
export function SearchableSelect({
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
81
|
+
export function SearchableSelect(allProps: SearchableSelectProps) {
|
|
82
|
+
const {
|
|
83
|
+
options,
|
|
84
|
+
value = null,
|
|
85
|
+
onValueChange,
|
|
86
|
+
placeholder = 'Select...',
|
|
87
|
+
searchPlaceholder = 'Search...',
|
|
88
|
+
emptyText = 'No results found',
|
|
89
|
+
loadingText = 'Loading…',
|
|
90
|
+
isLoading = false,
|
|
91
|
+
disabled = false,
|
|
92
|
+
trigger,
|
|
93
|
+
id,
|
|
94
|
+
label,
|
|
95
|
+
labelVariant,
|
|
96
|
+
error,
|
|
97
|
+
layout = 'list',
|
|
98
|
+
renderOption,
|
|
99
|
+
align = 'start',
|
|
100
|
+
className,
|
|
101
|
+
contentClassName,
|
|
102
|
+
} = allProps;
|
|
103
|
+
// Wired for a message even while `error` is undefined — see `FieldWrapper.errorSlot`.
|
|
104
|
+
const errorSlot = 'error' in allProps;
|
|
105
|
+
const isInvalid = !!error;
|
|
106
|
+
const isGrid = layout === 'grid';
|
|
107
|
+
// The label names the trigger through `htmlFor`: clicking it focuses the
|
|
108
|
+
// button and assistive tech reads the field by it. A caller's `id` wins.
|
|
109
|
+
const generatedId = useId();
|
|
110
|
+
const triggerId = id ?? generatedId;
|
|
111
|
+
|
|
69
112
|
const [isOpen, setIsOpen] = useState(false);
|
|
70
113
|
const [search, setSearch] = useState('');
|
|
71
114
|
const keyboardPadding = useKeyboardCollisionPadding();
|
|
@@ -79,7 +122,9 @@ export function SearchableSelect({
|
|
|
79
122
|
const filtered = useMemo(() => {
|
|
80
123
|
const q = search.trim().toLowerCase();
|
|
81
124
|
if (!q) return options;
|
|
82
|
-
return options.filter(
|
|
125
|
+
return options.filter(
|
|
126
|
+
o => o.label.toLowerCase().includes(q) || (o.keywords ?? []).some(k => k.toLowerCase().includes(q)),
|
|
127
|
+
);
|
|
83
128
|
}, [options, search]);
|
|
84
129
|
|
|
85
130
|
const selectedOption = useMemo(
|
|
@@ -94,8 +139,10 @@ export function SearchableSelect({
|
|
|
94
139
|
|
|
95
140
|
const defaultTrigger = (
|
|
96
141
|
<button
|
|
142
|
+
id={triggerId}
|
|
97
143
|
type="button"
|
|
98
144
|
disabled={disabled}
|
|
145
|
+
data-invalid={isInvalid || undefined}
|
|
99
146
|
className={cn(
|
|
100
147
|
// Mirrors SelectTrigger so the closed state is indistinguishable from
|
|
101
148
|
// the regular Select.
|
|
@@ -103,22 +150,45 @@ export function SearchableSelect({
|
|
|
103
150
|
'border-ods-border bg-ods-card text-h4',
|
|
104
151
|
selectedOption ? 'text-ods-text-primary' : 'text-ods-text-secondary',
|
|
105
152
|
'enabled:hover:border-ods-border-hover enabled:hover:bg-ods-bg-hover enabled:active:border-ods-border-active enabled:active:bg-ods-bg-active',
|
|
106
|
-
'data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent',
|
|
153
|
+
!isInvalid && 'data-[state=open]:border-ods-accent data-[state=open]:hover:border-ods-accent',
|
|
107
154
|
'group cursor-pointer transition-colors duration-200 disabled:!cursor-not-allowed disabled:bg-ods-bg',
|
|
155
|
+
isInvalid &&
|
|
156
|
+
'border-ods-error enabled:hover:border-ods-error enabled:active:border-ods-error data-[state=open]:border-ods-error',
|
|
108
157
|
className,
|
|
109
158
|
)}
|
|
110
159
|
>
|
|
111
|
-
<span className="
|
|
160
|
+
<span className="flex min-w-0 flex-1 items-center gap-2">
|
|
161
|
+
{selectedOption?.icon}
|
|
162
|
+
<span className="truncate">{selectedOption ? selectedOption.label : placeholder}</span>
|
|
163
|
+
</span>
|
|
112
164
|
<Chevron02DownIcon
|
|
113
|
-
className=
|
|
165
|
+
className={cn(
|
|
166
|
+
'shrink-0 text-ods-text-secondary transition-all duration-200 group-data-[state=open]:rotate-180',
|
|
167
|
+
isInvalid ? 'text-ods-error' : 'group-data-[state=open]:text-ods-accent',
|
|
168
|
+
)}
|
|
114
169
|
size={24}
|
|
115
170
|
/>
|
|
116
171
|
</button>
|
|
117
172
|
);
|
|
118
173
|
|
|
174
|
+
const triggerElement = <PopoverPrimitive.Trigger asChild>{trigger ?? defaultTrigger}</PopoverPrimitive.Trigger>;
|
|
175
|
+
|
|
176
|
+
// A `role="listbox"` child that is a status line, not an option: in the grid
|
|
177
|
+
// it has to span the row or it would sit in one tile-sized cell.
|
|
178
|
+
const statusClass = cn(
|
|
179
|
+
'px-[var(--spacing-system-sf)] py-[var(--spacing-system-s)] text-ods-text-secondary text-h5',
|
|
180
|
+
isGrid && 'col-span-full',
|
|
181
|
+
);
|
|
182
|
+
|
|
119
183
|
return (
|
|
120
184
|
<PopoverPrimitive.Root open={isOpen} onOpenChange={disabled ? undefined : setIsOpen} modal={false}>
|
|
121
|
-
|
|
185
|
+
{!trigger && (label !== undefined || errorSlot) ? (
|
|
186
|
+
<FieldWrapper label={label} htmlFor={triggerId} labelVariant={labelVariant} error={error} errorSlot={errorSlot}>
|
|
187
|
+
{triggerElement}
|
|
188
|
+
</FieldWrapper>
|
|
189
|
+
) : (
|
|
190
|
+
triggerElement
|
|
191
|
+
)}
|
|
122
192
|
<PopoverPrimitive.Portal>
|
|
123
193
|
<PopoverPrimitive.Content
|
|
124
194
|
align={align}
|
|
@@ -147,15 +217,22 @@ export function SearchableSelect({
|
|
|
147
217
|
className="rounded-none border-0"
|
|
148
218
|
/>
|
|
149
219
|
</div>
|
|
150
|
-
<div
|
|
220
|
+
<div
|
|
221
|
+
className={cn(
|
|
222
|
+
'max-h-80 min-h-0 overflow-y-auto',
|
|
223
|
+
// Tiles grow from 7rem: the popover's own border and a classic
|
|
224
|
+
// always-on scrollbar both eat into the track, so 7.5rem (the
|
|
225
|
+
// Figma cell) dropped a 360px popover to two columns. The column
|
|
226
|
+
// count follows the popover width. Each tile draws its own
|
|
227
|
+
// bottom/right rule, so the grid needs no gap.
|
|
228
|
+
isGrid ? 'grid grid-cols-[repeat(auto-fill,minmax(7rem,1fr))]' : 'py-[var(--spacing-system-xs)]',
|
|
229
|
+
)}
|
|
230
|
+
role="listbox"
|
|
231
|
+
>
|
|
151
232
|
{isLoading ? (
|
|
152
|
-
<div className=
|
|
153
|
-
{loadingText}
|
|
154
|
-
</div>
|
|
233
|
+
<div className={statusClass}>{loadingText}</div>
|
|
155
234
|
) : filtered.length === 0 ? (
|
|
156
|
-
<div className=
|
|
157
|
-
{emptyText}
|
|
158
|
-
</div>
|
|
235
|
+
<div className={statusClass}>{emptyText}</div>
|
|
159
236
|
) : (
|
|
160
237
|
filtered.map(opt => {
|
|
161
238
|
const isSelected = value === opt.value;
|
|
@@ -167,13 +244,28 @@ export function SearchableSelect({
|
|
|
167
244
|
aria-selected={isSelected}
|
|
168
245
|
onClick={() => handleSelect(opt.value)}
|
|
169
246
|
className={cn(
|
|
170
|
-
'flex w-full items-center gap-[var(--spacing-system-xs)] px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',
|
|
171
247
|
'transition-colors hover:bg-ods-bg-hover',
|
|
248
|
+
isGrid
|
|
249
|
+
? 'flex min-w-0 flex-col items-center justify-center gap-[var(--spacing-system-xxs)] border-b border-r border-ods-border p-[var(--spacing-system-xs)]'
|
|
250
|
+
: 'flex w-full items-center gap-[var(--spacing-system-xs)] px-[var(--spacing-system-sf)] py-[var(--spacing-system-xs)] text-left',
|
|
172
251
|
isSelected && 'bg-ods-bg-hover',
|
|
173
252
|
)}
|
|
174
253
|
>
|
|
175
254
|
{renderOption ? (
|
|
176
255
|
renderOption(opt, isSelected)
|
|
256
|
+
) : isGrid ? (
|
|
257
|
+
<>
|
|
258
|
+
{opt.icon}
|
|
259
|
+
<span
|
|
260
|
+
className={cn(
|
|
261
|
+
'w-full truncate text-center text-h6',
|
|
262
|
+
isSelected ? 'text-ods-text-primary' : 'text-ods-text-secondary',
|
|
263
|
+
)}
|
|
264
|
+
title={opt.label}
|
|
265
|
+
>
|
|
266
|
+
{opt.label}
|
|
267
|
+
</span>
|
|
268
|
+
</>
|
|
177
269
|
) : (
|
|
178
270
|
<>
|
|
179
271
|
{opt.icon}
|